"""Tests for SQLite persistence of all dashboard data.

Covers:
  - Ticket, Sprint, Milestone save/load through SQLiteNamespaceStore
  - Account save/load
  - Environment save/load
  - Project save/load
  - User save/load
  - Data survives manager re-instantiation (simulating server restart)
  - PROXYM_DATA_DIR env var override
"""

from __future__ import annotations

import json
import os
import sqlite3
from pathlib import Path

import pytest

from proxym.storage import SQLiteNamespaceStore, is_sqlite_database


# ── SQLiteNamespaceStore unit tests ──────────────────────────


class TestSQLiteNamespaceStore:
    def test_save_and_load_namespace(self, tmp_path):
        db = tmp_path / "test.sqlite"
        store = SQLiteNamespaceStore(db)
        store.save_namespace("items", {"a": {"name": "alpha"}, "b": {"name": "beta"}})

        loaded = store.load_namespace("items")
        assert len(loaded) == 2
        assert loaded["a"]["name"] == "alpha"
        assert loaded["b"]["name"] == "beta"

    def test_save_namespaces_batch(self, tmp_path):
        db = tmp_path / "test.sqlite"
        store = SQLiteNamespaceStore(db)
        store.save_namespaces({
            "tickets": {"t1": {"task": "fix bug"}},
            "sprints": {"s1": {"name": "Sprint 1"}},
        })

        assert store.load_namespace("tickets")["t1"]["task"] == "fix bug"
        assert store.load_namespace("sprints")["s1"]["name"] == "Sprint 1"

    def test_save_overwrites_previous(self, tmp_path):
        db = tmp_path / "test.sqlite"
        store = SQLiteNamespaceStore(db)
        store.save_namespace("ns", {"a": {"v": 1}})
        store.save_namespace("ns", {"b": {"v": 2}})

        loaded = store.load_namespace("ns")
        assert "a" not in loaded
        assert loaded["b"]["v"] == 2

    def test_load_nonexistent_returns_empty(self, tmp_path):
        db = tmp_path / "test.sqlite"
        store = SQLiteNamespaceStore(db)
        assert store.load_namespace("missing") == {}

    def test_load_nonexistent_file_returns_empty(self, tmp_path):
        db = tmp_path / "nonexistent.sqlite"
        store = SQLiteNamespaceStore(db)
        assert store.load_namespace("anything") == {}

    def test_creates_parent_dirs(self, tmp_path):
        db = tmp_path / "deep" / "nested" / "dir" / "test.sqlite"
        store = SQLiteNamespaceStore(db)
        store.save_namespace("ns", {"a": {"v": 1}})
        assert db.exists()
        assert is_sqlite_database(db)

    def test_enum_values_serialized_with_default_str(self, tmp_path):
        """Verify that enum-like values survive json.dumps(default=str) round-trip."""
        from proxym.dashboard.tickets import Ticket, TicketPriority, TicketStatus

        db = tmp_path / "test.sqlite"
        store = SQLiteNamespaceStore(db)

        t = Ticket(task="test enum", priority=TicketPriority.HIGH, status=TicketStatus.IN_PROGRESS)
        store.save_namespace("tickets", {t.id: t.model_dump()})

        loaded = store.load_namespace("tickets")
        assert t.id in loaded
        reconstructed = Ticket(**loaded[t.id])
        assert reconstructed.status == TicketStatus.IN_PROGRESS
        assert reconstructed.priority == TicketPriority.HIGH


# ── TicketManager SQLite persistence ─────────────────────────


class TestTicketManagerSQLitePersistence:
    def test_tickets_persist_and_reload(self, tmp_path):
        from proxym.dashboard.tickets import Ticket, TicketManager, Sprint, Milestone

        db = tmp_path / "proxym.sqlite"
        mgr1 = TicketManager(persist_path=db)
        t = mgr1.create_ticket(Ticket(task="persist test", tags=["sqlite"]))
        mgr1.create_sprint(Sprint(name="Sprint A"))
        mgr1.create_milestone(Milestone(name="Milestone A"))

        # Re-instantiate — simulates server restart
        mgr2 = TicketManager(persist_path=db)
        assert len(mgr2.list_tickets()) == 1
        assert mgr2.get_ticket(t.id).task == "persist test"
        assert mgr2.get_ticket(t.id).tags == ["sqlite"]
        assert len(mgr2.list_sprints()) == 1
        assert mgr2.list_sprints()[0].name == "Sprint A"
        assert len(mgr2.list_milestones()) == 1

    def test_ticket_update_persists(self, tmp_path):
        from proxym.dashboard.tickets import Ticket, TicketManager, TicketStatus

        db = tmp_path / "proxym.sqlite"
        mgr1 = TicketManager(persist_path=db)
        t = mgr1.create_ticket(Ticket(task="update test"))
        mgr1.update_ticket(t.id, {"status": "done"})

        mgr2 = TicketManager(persist_path=db)
        reloaded = mgr2.get_ticket(t.id)
        assert reloaded.status == TicketStatus.DONE
        assert reloaded.completed_at > 0

    def test_tool_assignment_persists(self, tmp_path):
        from proxym.dashboard.tickets import Ticket, TicketManager, ToolAssignment

        db = tmp_path / "proxym.sqlite"
        mgr1 = TicketManager(persist_path=db)
        t = mgr1.create_ticket(Ticket(task="assign test"))
        mgr1.assign_tool(t.id, ToolAssignment(tool="aider", agent_mode="code", model="smart"))

        mgr2 = TicketManager(persist_path=db)
        reloaded = mgr2.get_ticket(t.id)
        assert reloaded.assigned_tool is not None
        assert reloaded.assigned_tool.tool == "aider"
        assert reloaded.assigned_tool.agent_mode == "code"

    def test_comments_persist(self, tmp_path):
        from proxym.dashboard.tickets import Ticket, TicketManager, TicketComment

        db = tmp_path / "proxym.sqlite"
        mgr1 = TicketManager(persist_path=db)
        t = mgr1.create_ticket(Ticket(task="comment test"))
        mgr1.add_comment(t.id, TicketComment(author="aider", content="Fix applied"))

        mgr2 = TicketManager(persist_path=db)
        reloaded = mgr2.get_ticket(t.id)
        assert len(reloaded.comments) == 1
        assert reloaded.comments[0].author == "aider"
        assert reloaded.comments[0].content == "Fix applied"

    def test_delete_ticket_persists(self, tmp_path):
        from proxym.dashboard.tickets import Ticket, TicketManager

        db = tmp_path / "proxym.sqlite"
        mgr1 = TicketManager(persist_path=db)
        t = mgr1.create_ticket(Ticket(task="delete test"))
        mgr1.delete_ticket(t.id)

        mgr2 = TicketManager(persist_path=db)
        assert mgr2.get_ticket(t.id) is None
        assert len(mgr2.list_tickets()) == 0

    def test_many_tickets_persist(self, tmp_path):
        from proxym.dashboard.tickets import Ticket, TicketManager

        db = tmp_path / "proxym.sqlite"
        mgr1 = TicketManager(persist_path=db)
        for i in range(50):
            mgr1.create_ticket(Ticket(task=f"ticket #{i}"))

        mgr2 = TicketManager(persist_path=db)
        assert len(mgr2.list_tickets()) == 50


# ── AccountManager SQLite persistence ────────────────────────


class TestAccountManagerSQLitePersistence:
    def test_account_persists(self, tmp_path):
        from proxym.accounts import Account, AccountManager, ProviderType

        db = tmp_path / "proxym.sqlite"
        mgr1 = AccountManager(config_path=db)
        acct = Account(name="Test", provider=ProviderType.ANTHROPIC)
        mgr1.add_account(acct)

        mgr2 = AccountManager(config_path=db)
        assert len(mgr2.accounts) == 1
        assert list(mgr2.accounts.values())[0].name == "Test"


# ── EnvironmentRegistry SQLite persistence ───────────────────


class TestEnvironmentRegistrySQLitePersistence:
    def test_environment_persists(self, tmp_path):
        from proxym.dashboard._environments_api import EnvironmentRegistry, Environment, EnvironmentKind

        db = tmp_path / "proxym.sqlite"
        reg1 = EnvironmentRegistry(persist_path=db)
        env = Environment(name="dev", kind=EnvironmentKind.LOCAL_PATH, path="/tmp/test")
        reg1.environments[env.id] = env
        reg1._save()

        reg2 = EnvironmentRegistry(persist_path=db)
        assert len(reg2.environments) == 1
        assert list(reg2.environments.values())[0].name == "dev"

    def test_tool_assignment_persists(self, tmp_path):
        from proxym.dashboard._environments_api import EnvironmentRegistry, Environment, EnvironmentKind

        db = tmp_path / "proxym.sqlite"
        reg1 = EnvironmentRegistry(persist_path=db)
        env = Environment(name="dev", kind=EnvironmentKind.LOCAL_PATH, path="/tmp/test")
        reg1.environments[env.id] = env
        reg1.tool_assignments["aider"] = env.id
        reg1._save()

        reg2 = EnvironmentRegistry(persist_path=db)
        assert reg2.tool_assignments.get("aider") == env.id


# ── ProjectRegistry SQLite persistence ───────────────────────


class TestProjectRegistrySQLitePersistence:
    def test_project_persists(self, tmp_path):
        from proxym.projects import Project, ProjectRegistry

        db = tmp_path / "proxym.sqlite"
        reg1 = ProjectRegistry(persist_path=db)
        proj = Project(name="test-project", source_path=str(tmp_path))
        reg1.add(proj)

        reg2 = ProjectRegistry(persist_path=db)
        assert len(reg2.list_projects()) == 1
        assert reg2.list_projects()[0].name == "test-project"


# ── UserManager SQLite persistence ───────────────────────────


class TestUserManagerSQLitePersistence:
    def test_user_persists(self, tmp_path):
        from proxym.dashboard.users import UserManager, User

        db = tmp_path / "proxym.sqlite"
        # Reset singleton
        UserManager.reset()
        mgr1 = UserManager.get(db)
        user = User(name="Test User", email="test@test.com", username="test")
        mgr1.create_user(user)

        UserManager.reset()
        mgr2 = UserManager.get(db)
        users = mgr2.list_users()
        assert len(users) >= 1
        assert any(u.email == "test@test.com" for u in users)


# ── PROXYM_DATA_DIR env var ──────────────────────────────────


class TestDataDirEnvVar:
    def test_proxym_data_dir_overrides_default(self, tmp_path, monkeypatch):
        custom_dir = tmp_path / "custom_data"
        custom_dir.mkdir()
        monkeypatch.setenv("PROXYM_DATA_DIR", str(custom_dir))

        # Re-import to pick up env var
        import importlib
        import proxym.storage
        importlib.reload(proxym.storage)

        assert proxym.storage.DEFAULT_SQLITE_PATH == custom_dir / "proxym.sqlite"

        # Restore
        monkeypatch.delenv("PROXYM_DATA_DIR", raising=False)
        importlib.reload(proxym.storage)

    def test_default_path_when_no_env(self, monkeypatch):
        monkeypatch.delenv("PROXYM_DATA_DIR", raising=False)

        import importlib
        import proxym.storage
        importlib.reload(proxym.storage)

        assert "proxym.sqlite" in str(proxym.storage.DEFAULT_SQLITE_PATH)
        assert ".local/share/proxym" in str(proxym.storage.DEFAULT_SQLITE_PATH)


# ── Cross-manager shared SQLite file ─────────────────────────


class TestSharedSQLiteFile:
    def test_all_managers_share_one_sqlite(self, tmp_path):
        """All managers should coexist in a single SQLite file without conflicts."""
        from proxym.dashboard.tickets import Ticket, TicketManager, Sprint, Milestone
        from proxym.accounts import Account, AccountManager, ProviderType
        from proxym.dashboard._environments_api import EnvironmentRegistry, Environment, EnvironmentKind
        from proxym.projects import Project, ProjectRegistry
        from proxym.dashboard.users import UserManager, User

        db = tmp_path / "proxym.sqlite"

        # Create data across all managers
        tmgr = TicketManager(persist_path=db)
        tmgr.create_ticket(Ticket(task="shared test ticket"))
        tmgr.create_sprint(Sprint(name="shared sprint"))
        tmgr.create_milestone(Milestone(name="shared milestone"))

        amgr = AccountManager(config_path=db)
        amgr.add_account(Account(name="Shared", provider=ProviderType.OPENAI))

        ereg = EnvironmentRegistry(persist_path=db)
        env = Environment(name="shared-env", kind=EnvironmentKind.LOCAL_PATH, path="/tmp")
        ereg.environments[env.id] = env
        ereg._save()

        preg = ProjectRegistry(persist_path=db)
        preg.add(Project(name="shared-project", source_path=str(tmp_path)))

        UserManager.reset()
        umgr = UserManager.get(db)
        umgr.create_user(User(name="Shared User", email="shared@test.com", username="shared"))

        # Verify all data in one SQLite file
        conn = sqlite3.connect(str(db))
        conn.row_factory = sqlite3.Row
        rows = conn.execute(
            "SELECT namespace, COUNT(*) as cnt FROM namespace_records GROUP BY namespace ORDER BY namespace"
        ).fetchall()
        namespaces = {r["namespace"]: r["cnt"] for r in rows}
        conn.close()

        assert namespaces.get("tickets", 0) >= 1
        assert namespaces.get("sprints", 0) >= 1
        assert namespaces.get("milestones", 0) >= 1
        assert namespaces.get("accounts", 0) >= 1
        assert namespaces.get("environments", 0) >= 1
        assert namespaces.get("projects", 0) >= 1
        assert namespaces.get("users", 0) >= 1

        # Reload all managers from the same file — verify data integrity
        tmgr2 = TicketManager(persist_path=db)
        assert len(tmgr2.list_tickets()) == 1
        assert len(tmgr2.list_sprints()) == 1

        amgr2 = AccountManager(config_path=db)
        assert len(amgr2.accounts) >= 1

        ereg2 = EnvironmentRegistry(persist_path=db)
        assert len(ereg2.environments) >= 1

        preg2 = ProjectRegistry(persist_path=db)
        assert len(preg2.list_projects()) >= 1

        UserManager.reset()
        umgr2 = UserManager.get(db)
        assert any(u.email == "shared@test.com" for u in umgr2.list_users())
        UserManager.reset()
