"""Tests for the Backup Panel browser page — HTML rendering, API endpoints, tier gating."""

from __future__ import annotations

import json
import pytest
import threading
from http.client import HTTPConnection, HTTPResponse
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import yaml

from vastian.tasks.backup import BackupManager, BackupConfig
from vastian.tasks.backup_panel import BackupPanel, BackupPanelHandler, BACKUP_PANEL_HTML


# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def backup_project(temp_dir: Path) -> Path:
    """Create a temp project tree for backup panel tests."""
    (temp_dir / "state" / "giga-outbox").mkdir(parents=True)
    (temp_dir / "config" / "local").mkdir(parents=True)
    (temp_dir / "config" / "backup").mkdir(parents=True)
    (temp_dir / "docs" / "henry" / "briefs").mkdir(parents=True)
    (temp_dir / "workspace").mkdir(parents=True)

    (temp_dir / "state" / "bus.db").write_text("db", encoding="utf-8")
    (temp_dir / "config" / "local" / "tokens.json").write_text("{}", encoding="utf-8")
    (temp_dir / "workspace" / "notes.md").write_text("Notes", encoding="utf-8")

    settings = {
        "provider": "google_drive",
        "backup_mode": "live",
        "schedule": {"enabled": False, "interval_hours": 24},
        "categories": {
            "system_files": {"provider": None, "includes": ["state/", "config/local/", "*.db"]},
            "user_files": {"provider": None, "includes": ["docs/", "workspace/", "saved_prompts/"]},
        },
        "tier_access": {
            "open": ["manual_backup"],
            "standard": ["manual_backup", "single_provider"],
            "elevated": ["manual_backup", "multi_provider", "scheduled", "restore_history"],
            "admin": ["all"],
        },
    }
    (temp_dir / "config" / "backup" / "settings.yaml").write_text(
        yaml.dump(settings), encoding="utf-8"
    )
    return temp_dir


@pytest.fixture
def mock_provider() -> MagicMock:
    provider = MagicMock()
    provider.upload = AsyncMock(return_value={"remote_path": "test", "size": 100})
    provider.download = AsyncMock(return_value={"local_path": "test", "size": 100})
    provider.list_files = AsyncMock(return_value=[])
    provider.delete = AsyncMock(return_value=True)
    provider.get_auth_url = MagicMock(return_value="https://auth.example.com")
    provider.is_authenticated = MagicMock(return_value=True)
    return provider


@pytest.fixture
def manager(backup_project: Path, mock_provider: MagicMock) -> BackupManager:
    return BackupManager(project_root=backup_project, provider=mock_provider)


@pytest.fixture
def panel(backup_project: Path, manager: BackupManager) -> BackupPanel:
    return BackupPanel(
        port=0,  # use port 0 for test (OS picks free port)
        project_root=backup_project,
        backup_manager=manager,
        user_tier="open",
    )


# ---------------------------------------------------------------------------
# HTML rendering tests
# ---------------------------------------------------------------------------


class TestBackupPanelHTML:
    def test_html_contains_provider_selector(self) -> None:
        assert "Google Drive" in BACKUP_PANEL_HTML
        assert "Dropbox" in BACKUP_PANEL_HTML
        assert "Box" in BACKUP_PANEL_HTML

    def test_html_contains_backup_trigger_button(self) -> None:
        assert "Back Up Now" in BACKUP_PANEL_HTML
        assert "backup-trigger" in BACKUP_PANEL_HTML

    def test_html_contains_restore_section(self) -> None:
        assert "restore-section" in BACKUP_PANEL_HTML
        assert "Restore" in BACKUP_PANEL_HTML

    def test_html_contains_schedule_config(self) -> None:
        assert "schedule-toggle" in BACKUP_PANEL_HTML
        assert "schedule-interval" in BACKUP_PANEL_HTML

    def test_html_contains_file_category_split(self) -> None:
        assert "System Files" in BACKUP_PANEL_HTML
        assert "User Files" in BACKUP_PANEL_HTML

    def test_html_contains_tier_gating_indicators(self) -> None:
        assert "__SCHEDULE_TIER_CLASS__" in BACKUP_PANEL_HTML
        assert "tier-locked" in BACKUP_PANEL_HTML

    def test_html_contains_config_save_button(self) -> None:
        assert "config-save" in BACKUP_PANEL_HTML
        assert "Save" in BACKUP_PANEL_HTML

    def test_html_contains_config_reset_dropdown(self) -> None:
        assert "config-reset-dropdown" in BACKUP_PANEL_HTML
        assert "Restore from snapshot" in BACKUP_PANEL_HTML


# ---------------------------------------------------------------------------
# Server lifecycle tests
# ---------------------------------------------------------------------------


class TestBackupPanelServer:
    @pytest.fixture
    def running_panel(self, backup_project: Path, manager: BackupManager):
        """Start a panel on port 0 (OS-assigned) and tear down after test."""
        import time

        p = BackupPanel(
            port=0,
            project_root=backup_project,
            backup_manager=manager,
            user_tier="open",
        )
        p.start()
        time.sleep(0.3)  # allow server thread to start accepting on Windows
        yield p, p.port
        p.stop()

    def test_start_and_stop(self, running_panel) -> None:
        panel, port = running_panel
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request("GET", "/")
        resp = conn.getresponse()
        assert resp.status == 200
        conn.close()

    def test_serve_html_page(self, running_panel) -> None:
        panel, port = running_panel
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request("GET", "/")
        resp = conn.getresponse()
        body = resp.read().decode("utf-8")
        assert "Backup Panel" in body
        assert "text/html" in resp.getheader("Content-Type")
        conn.close()

    def test_api_backup_status(self, running_panel) -> None:
        panel, port = running_panel
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request("GET", "/api/backup/status")
        resp = conn.getresponse()
        data = json.loads(resp.read())
        assert resp.status == 200
        assert data["provider"] == "google_drive"
        assert "last_backup" in data
        assert "file_count" in data
        conn.close()

    def test_api_trigger_backup(self, running_panel, manager: BackupManager) -> None:
        panel, port = running_panel
        # Patch create_backup_manager so trigger uses our mock provider (panel recreates manager on each trigger)
        with patch("vastian.tasks.backup_panel.create_backup_manager", return_value=manager):
            conn = HTTPConnection("127.0.0.1", port, timeout=5)
            conn.request("POST", "/api/backup/trigger")
            resp = conn.getresponse()
            data = json.loads(resp.read())
        if resp.status != 200:
            pytest.fail(f"Expected 200, got {resp.status}: {data}")
        assert data["status"] == "completed"
        conn.close()

    def test_api_list_backups(self, running_panel) -> None:
        panel, port = running_panel
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request("GET", "/api/backup/history")
        resp = conn.getresponse()
        data = json.loads(resp.read())
        assert resp.status == 200
        assert "backups" in data
        conn.close()

    def test_api_restore_backup(self, running_panel) -> None:
        panel, port = running_panel
        body = json.dumps({"backup_id": "state/bus.db"}).encode("utf-8")
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request(
            "POST",
            "/api/backup/restore",
            body=body,
            headers={"Content-Type": "application/json", "Content-Length": str(len(body))},
        )
        resp = conn.getresponse()
        data = json.loads(resp.read())
        assert resp.status == 200
        conn.close()

    def test_api_update_settings(self, running_panel) -> None:
        """Settings update requires elevated tier; open tier should get 403."""
        panel, port = running_panel
        body = json.dumps({"schedule": {"enabled": True}}).encode("utf-8")
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request(
            "POST",
            "/api/backup/settings",
            body=body,
            headers={"Content-Type": "application/json", "Content-Length": str(len(body))},
        )
        resp = conn.getresponse()
        data = json.loads(resp.read())
        # Open tier should be blocked from schedule settings
        assert resp.status == 403
        conn.close()

    def test_api_save_config(self, running_panel, backup_project: Path) -> None:
        panel, port = running_panel
        body = json.dumps(
            {
                "file": "config/backup/settings.yaml",
                "content": "provider: dropbox\n",
            }
        ).encode("utf-8")
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request(
            "POST",
            "/api/config/save",
            body=body,
            headers={"Content-Type": "application/json", "Content-Length": str(len(body))},
        )
        resp = conn.getresponse()
        data = json.loads(resp.read())
        assert resp.status == 200
        assert data["status"] == "saved"
        conn.close()

    def test_api_list_config_snapshots(self, running_panel, manager: BackupManager) -> None:
        panel, port = running_panel
        # Create a snapshot first
        manager.save_config_snapshot("config/backup/settings.yaml")
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request("GET", "/api/config/snapshots?file=config/backup/settings.yaml")
        resp = conn.getresponse()
        data = json.loads(resp.read())
        assert resp.status == 200
        assert len(data["snapshots"]) >= 1
        conn.close()

    def test_api_restore_config_snapshot(self, running_panel, manager: BackupManager) -> None:
        panel, port = running_panel
        manager.save_config_snapshot("config/backup/settings.yaml", "test-snap")
        body = json.dumps(
            {
                "file": "config/backup/settings.yaml",
                "snapshot_id": "test-snap",
            }
        ).encode("utf-8")
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request(
            "POST",
            "/api/config/restore",
            body=body,
            headers={"Content-Type": "application/json", "Content-Length": str(len(body))},
        )
        resp = conn.getresponse()
        data = json.loads(resp.read())
        assert resp.status == 200
        assert data["status"] == "restored"
        conn.close()

    def test_api_reset_config_to_default(self, running_panel, backup_project: Path) -> None:
        panel, port = running_panel
        # Create a template
        template = backup_project / "config" / "backup" / "settings.example.yaml"
        template.write_text("provider: none\n", encoding="utf-8")
        body = json.dumps({"file": "config/backup/settings.yaml"}).encode("utf-8")
        conn = HTTPConnection("127.0.0.1", port, timeout=5)
        conn.request(
            "POST",
            "/api/config/reset",
            body=body,
            headers={"Content-Type": "application/json", "Content-Length": str(len(body))},
        )
        resp = conn.getresponse()
        data = json.loads(resp.read())
        assert resp.status == 200
        assert data["status"] == "reset"
        conn.close()


# ---------------------------------------------------------------------------
# Tier gating tests
# ---------------------------------------------------------------------------


class TestTierGating:
    def _make_panel(self, backup_project: Path, manager: BackupManager, tier: str):
        """Create a panel with a specific user tier on port 0 (OS-assigned)."""
        p = BackupPanel(
            port=0,
            project_root=backup_project,
            backup_manager=manager,
            user_tier=tier,
        )
        p.start()
        # Longer delay for Windows — the OS needs time to fully bind the socket
        import time

        time.sleep(1.0)
        return p, p.port

    def _post_settings(self, port: int, body_dict: dict) -> HTTPResponse:
        """Send a POST to /api/backup/settings with retry for Windows sockets."""
        import time

        body = json.dumps(body_dict).encode("utf-8")
        last_err = None
        for attempt in range(3):
            try:
                conn = HTTPConnection("127.0.0.1", port, timeout=5)
                conn.request(
                    "POST",
                    "/api/backup/settings",
                    body=body,
                    headers={"Content-Type": "application/json", "Content-Length": str(len(body))},
                )
                resp = conn.getresponse()
                conn.close()
                return resp
            except (ConnectionError, OSError) as exc:
                last_err = exc
                time.sleep(0.5)
        raise last_err  # type: ignore[misc]

    def test_tier_gating_blocks_schedule_for_open_tier(
        self, backup_project: Path, manager: BackupManager
    ) -> None:
        panel, port = self._make_panel(backup_project, manager, "open")
        try:
            resp = self._post_settings(port, {"schedule": {"enabled": True}})
            assert resp.status == 403
        finally:
            panel.stop()

    def test_tier_gating_allows_schedule_for_elevated_tier(
        self, backup_project: Path, manager: BackupManager
    ) -> None:
        panel, port = self._make_panel(backup_project, manager, "elevated")
        try:
            resp = self._post_settings(port, {"schedule": {"enabled": True}})
            assert resp.status == 200
        finally:
            panel.stop()


# ---------------------------------------------------------------------------
# Rendering tests
# ---------------------------------------------------------------------------


class TestBackupPanelRendering:
    def test_render_replaces_placeholders(self, panel: BackupPanel, manager: BackupManager) -> None:
        panel.backup_manager = manager
        html = panel.render_html()
        assert "__PROVIDER__" not in html
        assert "__LAST_BACKUP__" not in html
        assert "__FILE_COUNT_SYSTEM__" not in html
        assert "__FILE_COUNT_USER__" not in html
        assert "__SCHEDULE_TIER_CLASS__" not in html

    def test_render_shows_connected_provider(
        self, panel: BackupPanel, manager: BackupManager
    ) -> None:
        panel.backup_manager = manager
        html = panel.render_html()
        assert "google_drive" in html

    def test_render_shows_last_backup_time(
        self, panel: BackupPanel, manager: BackupManager
    ) -> None:
        panel.backup_manager = manager
        html = panel.render_html()
        # No backup yet, should show "Never"
        assert "Never" in html

    def test_render_shows_file_counts_by_category(
        self, panel: BackupPanel, manager: BackupManager
    ) -> None:
        panel.backup_manager = manager
        html = panel.render_html()
        assert "System Files" in html
        assert "User Files" in html
