"""Tests to boost code coverage for all view modules below 100%.

Covers: api_tokens, notifications, shared_links, share, plans,
imap_accounts, integrations, general, filemanager, files, help.
"""

import os
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app.config import settings as app_settings
from app.database import Base, get_db
from app.main import app

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def _fresh_db():
    """Yield a fresh in-memory SQLite session."""
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    session = sessionmaker(autocommit=False, autoflush=False, bind=engine)()
    try:
        yield session
    finally:
        session.close()
        Base.metadata.drop_all(bind=engine)


@pytest.fixture()
def client_fresh(_fresh_db) -> TestClient:
    """TestClient backed by a fresh database."""

    def _override():
        try:
            yield _fresh_db
        finally:
            pass

    app.dependency_overrides[get_db] = _override
    with TestClient(app, base_url="http://localhost") as tc:
        yield tc
    app.dependency_overrides.clear()


# ===================================================================
# 1. Simple template-render views (api_tokens, notifications,
#    shared_links, share, plans)
# ===================================================================


class TestApiTokensView:
    """GET /api-tokens should render the management page."""

    @pytest.mark.unit
    def test_api_tokens_page_returns_200(self, client_fresh: TestClient):
        resp = client_fresh.get("/api-tokens")
        assert resp.status_code == 200
        assert "API Tokens" in resp.text


class TestNotificationsView:
    """GET /notifications should render the dashboard."""

    @pytest.mark.unit
    def test_notifications_page_returns_200(self, client_fresh: TestClient):
        resp = client_fresh.get("/notifications")
        assert resp.status_code == 200
        assert "Notifications" in resp.text


class TestSharedLinksView:
    """GET /shared-links should render the management page."""

    @pytest.mark.unit
    def test_shared_links_page_returns_200(self, client_fresh: TestClient):
        resp = client_fresh.get("/shared-links")
        assert resp.status_code == 200
        assert "Shared Links" in resp.text


class TestShareView:
    """GET /share/{token} should render the public share landing page."""

    @pytest.mark.unit
    def test_share_page_returns_200(self, client_fresh: TestClient):
        resp = client_fresh.get("/share/abc123")
        assert resp.status_code == 200
        # Token should be passed to the template
        assert "abc123" in resp.text


class TestPlansViews:
    """GET /admin/plans and /admin/stripe-wizard should render pages."""

    @pytest.mark.unit
    def test_plan_designer_returns_200(self, client_fresh: TestClient):
        resp = client_fresh.get("/admin/plans")
        assert resp.status_code == 200

    @pytest.mark.unit
    def test_stripe_wizard_returns_200(self, client_fresh: TestClient):
        resp = client_fresh.get("/admin/stripe-wizard")
        assert resp.status_code == 200


# ===================================================================
# 2. imap_accounts view  (30.95 % → 100 %)
# ===================================================================


class TestImapAccountsView:
    """Tests for /imap-accounts view."""

    @pytest.mark.unit
    def test_imap_accounts_page_no_owner(self, client_fresh: TestClient):
        """When no owner_id is resolved, page renders with defaults."""
        resp = client_fresh.get("/imap-accounts")
        assert resp.status_code == 200

    @pytest.mark.unit
    def test_imap_accounts_page_with_owner(self, _fresh_db, client_fresh: TestClient):
        """When session has a user, the page queries IMAP accounts."""
        with patch("app.views.imap_accounts.get_current_owner_id", return_value="testuser"):
            with patch(
                "app.views.imap_accounts.get_user_tier_id",
                return_value="starter",
            ):
                with patch(
                    "app.views.imap_accounts.get_tier",
                    return_value={"id": "starter", "name": "Starter", "max_mailboxes": 3},
                ):
                    resp = client_fresh.get("/imap-accounts")
        assert resp.status_code == 200

    @pytest.mark.unit
    def test_get_max_mailboxes_free_tier(self):
        """Free tier should return 0 mailboxes."""
        from app.views.imap_accounts import _get_max_mailboxes

        assert _get_max_mailboxes({"id": "free", "max_mailboxes": 0}) == 0

    @pytest.mark.unit
    def test_get_max_mailboxes_unlimited(self):
        """When max_mailboxes is 0 on a non-free tier, it means unlimited."""
        from app.views.imap_accounts import _get_max_mailboxes

        assert _get_max_mailboxes({"id": "power", "max_mailboxes": 0}) is None

    @pytest.mark.unit
    def test_get_max_mailboxes_limited(self):
        """When max_mailboxes > 0, return that value."""
        from app.views.imap_accounts import _get_max_mailboxes

        assert _get_max_mailboxes({"id": "starter", "max_mailboxes": 5}) == 5


# ===================================================================
# 3. integrations view  (82.09 % → 100 %)
# ===================================================================


class TestIntegrationsView:
    """Tests for /integrations view."""

    @pytest.mark.unit
    def test_integrations_dashboard_no_owner(self, client_fresh: TestClient):
        """When no owner, the dashboard renders with zero-count defaults."""
        resp = client_fresh.get("/integrations")
        assert resp.status_code == 200

    @pytest.mark.unit
    def test_integrations_dashboard_with_owner(self, _fresh_db, client_fresh: TestClient):
        """When an owner_id is resolved, DB queries run and tier is fetched."""
        with patch("app.views.integrations.get_current_owner_id", return_value="testuser"):
            with patch("app.views.integrations.get_user_tier_id", return_value="power"):
                with patch(
                    "app.views.integrations.get_tier",
                    return_value={
                        "id": "power",
                        "name": "Power",
                        "max_storage_destinations": 0,
                        "max_mailboxes": 0,
                    },
                ):
                    resp = client_fresh.get("/integrations")
        assert resp.status_code == 200

    @pytest.mark.unit
    def test_integrations_dashboard_generic_exception(self, client_fresh: TestClient):
        """A non-HTTP exception in the dashboard returns 500."""
        with patch(
            "app.views.integrations.get_current_owner_id",
            side_effect=RuntimeError("boom"),
        ):
            resp = client_fresh.get("/integrations")
        assert resp.status_code == 500

    @pytest.mark.unit
    def test_integrations_dashboard_http_exception_passthrough(self, client_fresh: TestClient):
        """An HTTPException inside the dashboard is re-raised, not wrapped in 500."""
        from fastapi import HTTPException

        with patch(
            "app.views.integrations.get_current_owner_id",
            side_effect=HTTPException(status_code=403, detail="Forbidden"),
        ):
            resp = client_fresh.get("/integrations")
        assert resp.status_code == 403

    @pytest.mark.unit
    def test_get_max_destinations_free_default(self):
        from app.views.integrations import _get_max_destinations

        assert _get_max_destinations({"id": "free", "max_storage_destinations": 0}) == 1

    @pytest.mark.unit
    def test_get_max_destinations_free_with_value(self):
        from app.views.integrations import _get_max_destinations

        assert _get_max_destinations({"id": "free", "max_storage_destinations": 3}) == 3

    @pytest.mark.unit
    def test_get_max_destinations_unlimited(self):
        from app.views.integrations import _get_max_destinations

        assert _get_max_destinations({"id": "power", "max_storage_destinations": 0}) is None

    @pytest.mark.unit
    def test_get_max_destinations_limited(self):
        from app.views.integrations import _get_max_destinations

        assert _get_max_destinations({"id": "starter", "max_storage_destinations": 5}) == 5

    @pytest.mark.unit
    def test_get_max_sources_free(self):
        from app.views.integrations import _get_max_sources

        assert _get_max_sources({"id": "free", "max_mailboxes": 0}) == 0

    @pytest.mark.unit
    def test_get_max_sources_unlimited(self):
        from app.views.integrations import _get_max_sources

        assert _get_max_sources({"id": "power", "max_mailboxes": 0}) is None

    @pytest.mark.unit
    def test_get_max_sources_limited(self):
        from app.views.integrations import _get_max_sources

        assert _get_max_sources({"id": "starter", "max_mailboxes": 2}) == 2


# ===================================================================
# 4. general view  (88.68 % → 100 %)
# ===================================================================


class TestGeneralViewMultiUser:
    """Cover the multi_user_enabled subscription branch (lines 96-105)."""

    @staticmethod
    def _signed_session(user_data: dict) -> str:
        """Create a signed Starlette session cookie containing user_data."""
        import json
        from base64 import b64encode

        from itsdangerous import TimestampSigner

        secret = os.environ.get(
            "SESSION_SECRET",
            "test_secret_key_for_testing_must_be_at_least_32_characters_long",
        )
        signer = TimestampSigner(secret)
        data = {"user": user_data}
        return signer.sign(b64encode(json.dumps(data).encode("utf-8"))).decode("utf-8")

    @pytest.mark.unit
    def test_home_page_multi_user_with_subscription(self, _fresh_db, client_fresh: TestClient):
        """When multi_user_enabled is True and user has owner_id, subscription info is fetched.

        Lines 96-103: Exercises the subscription lookup path.
        """
        cookie_val = self._signed_session({"username": "testuser", "email": "test@example.com", "is_admin": False})
        tier_mock = {
            "id": "starter",
            "name": "Starter",
            "lifetime_file_limit": 1000,
            "daily_upload_limit": 50,
            "monthly_upload_limit": 500,
        }
        usage_mock = {"lifetime": 10, "today": 2, "month": 8}
        with (
            patch.object(app_settings, "multi_user_enabled", True),
            patch("app.utils.setup_wizard.is_setup_required", return_value=False),
            patch("app.views.general.get_provider_status", return_value={}),
            patch("app.views.general.validate_storage_configs", return_value={}),
            patch("app.utils.subscription.get_user_tier_id", return_value="starter"),
            patch("app.utils.subscription.get_tier", return_value=tier_mock),
            patch("app.utils.subscription.get_user_usage", return_value=usage_mock),
        ):
            client_fresh.cookies.set("session", cookie_val)
            resp = client_fresh.get("/?setup=complete")
        assert resp.status_code == 200

    @pytest.mark.unit
    def test_home_page_multi_user_subscription_error(self, _fresh_db, client_fresh: TestClient):
        """When subscription lookup fails, error is logged but page still renders.

        Lines 104-105: Exercises the exception handling branch.
        """
        cookie_val = self._signed_session({"username": "testuser", "email": "test@example.com", "is_admin": False})
        with (
            patch.object(app_settings, "multi_user_enabled", True),
            patch("app.utils.setup_wizard.is_setup_required", return_value=False),
            patch("app.views.general.get_provider_status", return_value={}),
            patch("app.views.general.validate_storage_configs", return_value={}),
            patch("app.utils.subscription.get_user_tier_id", side_effect=RuntimeError("boom")),
        ):
            client_fresh.cookies.set("session", cookie_val)
            resp = client_fresh.get("/?setup=complete")
        assert resp.status_code == 200


# ===================================================================
# 5. filemanager view  (96.63 % → 100 %)
# ===================================================================


class TestFilemanagerCoverageGaps:
    """Cover the remaining gaps in filemanager.py."""

    @pytest.mark.unit
    def test_format_size_petabytes(self):
        """Line 43: _format_size should return PB for very large sizes."""
        from app.views.filemanager import _format_size

        # 1 PB = 1024^5 bytes
        one_pb = 1024**5
        result = _format_size(one_pb)
        assert "PB" in result
        assert "1.0 PB" == result

    @pytest.mark.unit
    def test_format_size_multiple_petabytes(self):
        """Large values above 1 PB."""
        from app.views.filemanager import _format_size

        result = _format_size(5 * 1024**5)
        assert "PB" in result

    @pytest.mark.unit
    def test_scan_dir_with_broken_symlink(self, tmp_path):
        """Lines 104-106: files that cannot be stat'd are skipped with a warning.

        Using a broken symlink to trigger OSError on stat().
        """
        from app.views.filemanager import _scan_dir

        # Create a broken symlink — stat() will raise FileNotFoundError (subclass of OSError)
        broken_link = tmp_path / "broken_link.txt"
        broken_link.symlink_to("/nonexistent/target/file")

        # Also create a valid file so we can verify it's included
        valid_file = tmp_path / "valid.txt"
        valid_file.write_text("hello")

        db_paths: set[str] = set()
        entries = _scan_dir(tmp_path, tmp_path, db_paths)

        # The broken symlink should be skipped, the valid file should be included
        entry_names = [e["name"] for e in entries]
        assert "broken_link.txt" not in entry_names
        assert "valid.txt" in entry_names

    @pytest.mark.unit
    def test_scan_dir_oserror(self, tmp_path):
        """OSError during stat in _scan_dir is caught and file is skipped.

        We create a second broken symlink for this test.
        """
        from app.views.filemanager import _scan_dir

        broken_link = tmp_path / "also_broken.txt"
        broken_link.symlink_to("/another/nonexistent/path")

        valid_file = tmp_path / "good.txt"
        valid_file.write_text("ok")
        db_paths: set[str] = set()

        entries = _scan_dir(tmp_path, tmp_path, db_paths)
        entry_names = [e["name"] for e in entries]
        assert "also_broken.txt" not in entry_names
        assert "good.txt" in entry_names

    @pytest.mark.unit
    def test_walk_all_files_with_broken_symlink(self, tmp_path):
        """Lines 146-147: files that fail stat during walk are skipped.

        Using a broken symlink to trigger OSError.
        """
        from app.views.filemanager import _walk_all_files

        broken_link = tmp_path / "broken.pdf"
        broken_link.symlink_to("/nonexistent/target/file")

        valid_file = tmp_path / "valid.pdf"
        valid_file.write_text("content")

        db_paths: set[str] = set()
        entries = _walk_all_files(tmp_path, db_paths)

        entry_names = [e["name"] for e in entries]
        assert "broken.pdf" not in entry_names
        assert "valid.pdf" in entry_names


# ===================================================================
# 6. files view  (99.21 % → 100 %)
# ===================================================================


class TestFilesViewCoverageGaps:
    """Cover the remaining branches in files.py."""

    @pytest.mark.unit
    def test_compute_processing_flow_with_pipeline_steps(self):
        """Lines 504-515: pipeline_steps filtering in _compute_processing_flow."""
        from app.views.files import _compute_processing_flow

        # Create mock pipeline steps
        ps1 = SimpleNamespace(enabled=True, step_type="ocr")
        ps2 = SimpleNamespace(enabled=False, step_type="extract_metadata")
        ps3 = SimpleNamespace(enabled=True, step_type="send_to_destinations")

        # Create mock logs with all required attributes including task_id
        log1 = SimpleNamespace(
            step_name="create_file_record",
            status="completed",
            message="ok",
            timestamp=None,
            started_at=None,
            completed_at=None,
            task_id="task-001",
        )
        log2 = SimpleNamespace(
            step_name="check_text",
            status="completed",
            message="ok",
            timestamp=None,
            started_at=None,
            completed_at=None,
            task_id="task-002",
        )

        result = _compute_processing_flow([log1, log2], pipeline_steps=[ps1, ps2, ps3])

        # _compute_processing_flow returns a list of stage dicts
        stage_keys = [s["key"] for s in result]
        assert "create_file_record" in stage_keys  # always shown
        assert "check_text" in stage_keys  # OCR step type + ran
        # extract_metadata is disabled, so its stages should NOT be included
        assert "extract_metadata_with_gpt" not in stage_keys

    @pytest.mark.unit
    def test_compute_processing_flow_with_pipeline_steps_none(self):
        """When pipeline_steps is None, all stages are shown."""
        from app.views.files import _compute_processing_flow

        result = _compute_processing_flow([], pipeline_steps=None)
        stage_keys = [s["key"] for s in result]
        assert "create_file_record" in stage_keys
        assert "extract_metadata_with_gpt" in stage_keys

    @pytest.mark.unit
    def test_compute_processing_flow_with_empty_pipeline_steps(self):
        """When pipeline_steps is empty list, only always-show + ran stages remain."""
        from app.views.files import _compute_processing_flow

        result = _compute_processing_flow([], pipeline_steps=[])
        stage_keys = [s["key"] for s in result]
        assert "create_file_record" in stage_keys
        # Other stages should be filtered out
        assert "convert_to_pdf" not in stage_keys

    @pytest.mark.unit
    def test_compute_processing_flow_dedup_enabled(self):
        """When dedup is enabled and shown, check_for_duplicates stage appears."""
        from app.views.files import _compute_processing_flow

        with (
            patch.object(app_settings, "enable_deduplication", True),
            patch.object(app_settings, "show_deduplication_step", True),
        ):
            result = _compute_processing_flow([], pipeline_steps=None)
        stage_keys = [s["key"] for s in result]
        assert "check_for_duplicates" in stage_keys

    @pytest.mark.unit
    def test_file_detail_safe_exists_value_error(self, _fresh_db, client_fresh: TestClient):
        """Test that _safe_exists handles ValueError from commonpath gracefully.

        Lines 240-241: When os.path.commonpath raises ValueError (e.g., paths
        on different drives on Windows), _safe_exists returns False.
        """
        from app.models import FileRecord

        # Create a file record with all required fields
        rec = FileRecord(
            original_filename="test.pdf",
            local_filename="/tmp/test_local.pdf",
            original_file_path="/tmp/test_original.pdf",
            processed_file_path="/tmp/test_processed.pdf",
            file_size=100,
            mime_type="application/pdf",
            filehash="abc123def456",
        )
        _fresh_db.add(rec)
        _fresh_db.commit()
        _fresh_db.refresh(rec)

        # Patch commonpath to raise ValueError
        with patch("os.path.commonpath", side_effect=ValueError("different drives")):
            resp = client_fresh.get(f"/files/{rec.id}")

        assert resp.status_code == 200


# ===================================================================
# 7. help view  (96 % → 100 %)
# ===================================================================


class TestHelpViewCoverageGaps:
    """Cover the missing branch in help.py (34->37)."""

    @pytest.mark.unit
    def test_help_page_no_session_attr(self, client_fresh: TestClient):
        """When no session user is set, defaults are used for Zammad widgets."""
        resp = client_fresh.get("/help")
        assert resp.status_code == 200

    @pytest.mark.unit
    @pytest.mark.asyncio
    async def test_help_page_request_without_session(self):
        """Direct function call where request has no session attribute.

        Branch 34->37: when hasattr(request, 'session') is False.
        """
        from app.views.help import help_center

        # Create a mock request without session attribute
        mock_request = MagicMock(spec=[])  # spec=[] means no attributes
        # help_center checks hasattr(request, "session")
        # With spec=[], hasattr will return False

        with patch("app.views.help.templates") as mock_templates:
            mock_templates.TemplateResponse.return_value = "ok"
            await help_center(mock_request)

        # Template should be called with empty user context
        call_args = mock_templates.TemplateResponse.call_args
        ctx = call_args[0][1] if len(call_args[0]) > 1 else call_args[1].get("context", {})
        assert ctx["user_name"] == ""
        assert ctx["user_email"] == ""
        assert ctx["user_id"] == ""

    @pytest.mark.unit
    def test_help_page_with_logged_in_user(self, client_fresh: TestClient):
        """When session has user data, Zammad widget fields are populated.

        Covers lines 41-43 (user_name, user_email, user_id extraction).
        """
        import json
        from base64 import b64encode

        from itsdangerous import TimestampSigner

        secret = os.environ.get(
            "SESSION_SECRET",
            "test_secret_key_for_testing_must_be_at_least_32_characters_long",
        )
        signer = TimestampSigner(secret)
        session_data = {
            "user": {
                "name": "Jane Doe",
                "email": "jane@example.com",
                "preferred_username": "janedoe",
            }
        }
        cookie_val = signer.sign(b64encode(json.dumps(session_data).encode("utf-8"))).decode("utf-8")
        client_fresh.cookies.set("session", cookie_val)

        resp = client_fresh.get("/help")
        assert resp.status_code == 200
