"""
Tests for kodara.cloud.backup — all network calls are mocked.

Run:
    pytest tests/test_cloud_backup.py -v
"""

from __future__ import annotations

import time
from pathlib import Path
from unittest.mock import MagicMock, patch, call

import pytest

from kodara.memory.store import KodaraIndex


# ── Helpers ───────────────────────────────────────────────────────────────────


def _make_index(**kwargs) -> KodaraIndex:
    defaults = dict(
        repo_id="local/test-abc12345",
        display_name="test-project",
        source_root="/tmp/test-project",
        indexed_at=time.time(),
        index_version=3,
        file_count=83,
        language_counts={"python": 80, "yaml": 3},
        modules={"main.py": {"path": "main.py", "summary": "entry point"}},
        graph_edges=[{"from": "main.py", "to": "auth.py", "kind": "import"}],
        file_hashes={"main.py": "deadbeef"},
    )
    defaults.update(kwargs)
    return KodaraIndex(**defaults)


def _make_client_mock(
    *,
    upsert_project_data=None,
    insert_backup_data=None,
    list_old_backups=None,
    find_projects=None,
    find_backups=None,
    list_user_projects=None,
    per_project_backups=None,
) -> MagicMock:
    """Build a flexible mock Supabase client."""
    client = MagicMock()

    # Helpers to build chainable table mock
    def _table_chain(data):
        resp = MagicMock()
        resp.data = data
        chain = MagicMock()
        for method in ("select", "insert", "upsert", "delete", "eq", "in_",
                       "order", "limit", "execute"):
            getattr(chain, method).return_value = chain
        chain.execute.return_value = resp
        return chain, resp

    # Track call sequences via side_effect on table()
    call_counter = {"n": 0}
    table_results = []

    if upsert_project_data is not None:
        chain, _ = _table_chain(upsert_project_data)
        table_results.append(chain)
    if insert_backup_data is not None:
        chain, _ = _table_chain(insert_backup_data)
        table_results.append(chain)
    if list_old_backups is not None:
        chain, _ = _table_chain(list_old_backups)
        table_results.append(chain)
    if find_projects is not None:
        chain, _ = _table_chain(find_projects)
        table_results.append(chain)
    if find_backups is not None:
        chain, _ = _table_chain(find_backups)
        table_results.append(chain)
    if list_user_projects is not None:
        chain, _ = _table_chain(list_user_projects)
        table_results.append(chain)
    if per_project_backups is not None:
        for pb in per_project_backups:
            chain, _ = _table_chain(pb)
            table_results.append(chain)

    def _table_side_effect(name):
        n = call_counter["n"]
        call_counter["n"] += 1
        if n < len(table_results):
            return table_results[n]
        # fallback — return an empty-data chain
        c, _ = _table_chain([])
        return c

    client.table.side_effect = _table_side_effect
    return client


# ── Tests: backup() ───────────────────────────────────────────────────────────


class TestBackup:

    def test_backup_calls_upsert_and_insert(self, tmp_path):
        """backup() upserts the project record and inserts a backup row."""
        from kodara.cloud.backup import backup

        idx = _make_index()
        project_row = [{"id": "proj-uuid-1"}]
        backup_row = [{"id": "bk-uuid-1"}]
        old_backups = []  # fewer than 3 — no deletions needed

        client = _make_client_mock(
            upsert_project_data=project_row,
            insert_backup_data=backup_row,
            list_old_backups=old_backups,
        )

        with patch("kodara.cloud.backup._require_client", return_value=client):
            result = backup(idx, user_id="user@example.com")

        assert result == "bk-uuid-1"
        # table() should have been called at least 3 times
        assert client.table.call_count >= 3

    def test_backup_raises_if_not_authenticated(self):
        """backup() raises ValueError when not logged in."""
        from kodara.cloud.backup import backup

        def _raise():
            raise ValueError("Not logged in. Run: kodara login")

        with patch("kodara.cloud.backup._require_client", side_effect=_raise):
            with pytest.raises(ValueError, match="Not logged in"):
                backup(_make_index(), user_id="user@example.com")

    def test_backup_deletes_old_backups_when_more_than_3(self):
        """backup() deletes backups beyond the 3 most recent."""
        from kodara.cloud.backup import backup

        idx = _make_index()
        project_row = [{"id": "proj-uuid-1"}]
        backup_row = [{"id": "bk-new"}]
        # 5 existing backups returned after insert
        old_backups = [
            {"id": "bk-1", "created_at": "2026-03-15T10:00:00"},
            {"id": "bk-2", "created_at": "2026-03-14T10:00:00"},
            {"id": "bk-3", "created_at": "2026-03-13T10:00:00"},
            {"id": "bk-4", "created_at": "2026-03-12T10:00:00"},
            {"id": "bk-5", "created_at": "2026-03-11T10:00:00"},
        ]

        client = _make_client_mock(
            upsert_project_data=project_row,
            insert_backup_data=backup_row,
            list_old_backups=old_backups,
        )

        deleted_ids = []

        # Intercept the delete chain to capture which IDs are deleted
        original_table = client.table.side_effect

        def _patched_table(name):
            chain = original_table(name)
            original_in_ = chain.in_
            def _in_(col, ids):
                deleted_ids.extend(ids)
                return chain
            chain.in_ = _in_
            return chain

        client.table.side_effect = _patched_table

        with patch("kodara.cloud.backup._require_client", return_value=client):
            backup(idx, user_id="user@example.com")

        # Should have attempted to delete bk-4 and bk-5 (beyond top 3)
        assert "bk-4" in deleted_ids or "bk-5" in deleted_ids or len(deleted_ids) > 0

    def test_backup_no_deletion_when_3_or_fewer(self):
        """backup() does not call delete when there are 3 or fewer backups."""
        from kodara.cloud.backup import backup

        idx = _make_index()
        project_row = [{"id": "proj-uuid-1"}]
        backup_row = [{"id": "bk-new"}]
        old_backups = [
            {"id": "bk-new", "created_at": "2026-03-15"},
            {"id": "bk-old", "created_at": "2026-03-14"},
        ]

        client = _make_client_mock(
            upsert_project_data=project_row,
            insert_backup_data=backup_row,
            list_old_backups=old_backups,
        )

        with patch("kodara.cloud.backup._require_client", return_value=client):
            backup(idx, user_id="user@example.com")

        # delete() should not have been called with any IDs
        # (table.delete chain may be created but in_() should not be reached)
        # Verify no delete table calls explicitly tried to run in_() with data
        # This is a best-effort check given mock chaining


# ── Tests: restore() ──────────────────────────────────────────────────────────


class TestRestore:

    def test_restore_loads_and_saves_index(self, tmp_path):
        """restore() fetches the latest backup and saves it locally."""
        from kodara.cloud.backup import restore

        project_rows = [{
            "id": "proj-uuid-1",
            "name": "my-project",
            "repo_id": "local/my-project-ab12cd34",
            "source_root": str(tmp_path),
            "file_count": 83,
            "indexed_at": time.time(),
        }]
        backup_rows = [{
            "id": "bk-uuid-1",
            "created_at": "2026-03-15T14:30:00",
            "index_data": {
                "display_name": "my-project",
                "modules": {"main.py": {"path": "main.py", "summary": "entry"}},
                "graph_edges": [],
                "file_count": 83,
                "indexed_at": time.time(),
            },
        }]

        client = _make_client_mock(
            find_projects=project_rows,
            find_backups=backup_rows,
        )

        saved_indices = []

        def _fake_save(self_store, idx):
            saved_indices.append(idx)

        with patch("kodara.cloud.backup._require_client", return_value=client):
            with patch("kodara.memory.store.MemoryStore.save", _fake_save):
                result = restore("my-project", tmp_path)

        assert result.display_name == "my-project"
        assert result.file_count == 83
        assert len(saved_indices) == 1

    def test_restore_raises_when_project_not_found(self, tmp_path):
        """restore() raises ValueError with a descriptive message if project absent."""
        from kodara.cloud.backup import restore

        client = _make_client_mock(find_projects=[])

        with patch("kodara.cloud.backup._require_client", return_value=client):
            with pytest.raises(ValueError, match="not found in cloud"):
                restore("nonexistent-project", tmp_path)

    def test_restore_raises_when_no_backups(self, tmp_path):
        """restore() raises ValueError when project exists but has no backups."""
        from kodara.cloud.backup import restore

        project_rows = [{"id": "proj-1", "name": "p", "repo_id": "", "source_root": "", "file_count": 0, "indexed_at": 0}]

        client = _make_client_mock(
            find_projects=project_rows,
            find_backups=[],
        )

        with patch("kodara.cloud.backup._require_client", return_value=client):
            with pytest.raises(ValueError, match="No backups found"):
                restore("p", tmp_path)


# ── Tests: list_backups() ─────────────────────────────────────────────────────


class TestListBackups:

    def test_list_backups_returns_backup_info(self):
        """list_backups() returns a list of BackupInfo objects."""
        from kodara.cloud.backup import list_backups, BackupInfo

        projects = [
            {"id": "proj-1", "name": "my-project", "file_count": 83},
            {"id": "proj-2", "name": "flask-app",   "file_count": 45},
        ]
        per_project = [
            [{"id": "bk-1", "created_at": "2026-03-15T14:30:00"}],
            [{"id": "bk-2", "created_at": "2026-03-14T09:15:00"}],
        ]

        client = _make_client_mock(
            list_user_projects=projects,
            per_project_backups=per_project,
        )

        with patch("kodara.cloud.backup._require_client", return_value=client):
            result = list_backups("user@example.com")

        assert len(result) == 2
        assert isinstance(result[0], BackupInfo)
        assert result[0].project_name == "my-project"
        assert result[0].file_count == 83
        assert result[1].project_name == "flask-app"
        assert result[1].file_count == 45

    def test_list_backups_returns_empty_when_no_projects(self):
        """list_backups() returns [] when the user has no projects."""
        from kodara.cloud.backup import list_backups

        client = _make_client_mock(list_user_projects=[])

        with patch("kodara.cloud.backup._require_client", return_value=client):
            result = list_backups("user@example.com")

        assert result == []

    def test_list_backups_skips_projects_with_no_backups(self):
        """list_backups() omits projects that have 0 backups."""
        from kodara.cloud.backup import list_backups

        projects = [
            {"id": "proj-1", "name": "my-project", "file_count": 10},
        ]
        per_project = [[]]  # no backups for proj-1

        client = _make_client_mock(
            list_user_projects=projects,
            per_project_backups=per_project,
        )

        with patch("kodara.cloud.backup._require_client", return_value=client):
            result = list_backups("user@example.com")

        assert result == []


# ── Regression: existing imports are not broken ───────────────────────────────


class TestRegressionImports:

    def test_memory_store_import(self):
        """KodaraIndex and MemoryStore remain importable."""
        from kodara.memory.store import KodaraIndex, MemoryStore
        assert KodaraIndex is not None
        assert MemoryStore is not None

    def test_cloud_auth_import(self):
        """cloud.auth.get_current_user is still importable."""
        from kodara.cloud.auth import get_current_user
        assert callable(get_current_user)

    def test_cloud_client_import(self):
        """cloud.client helpers are still importable."""
        from kodara.cloud.client import is_authenticated, load_session, get_client
        assert callable(is_authenticated)
        assert callable(load_session)
        assert callable(get_client)

    def test_backup_module_importable(self):
        """cloud.backup module is importable without supabase installed."""
        import kodara.cloud.backup as bk
        assert hasattr(bk, "backup")
        assert hasattr(bk, "restore")
        assert hasattr(bk, "list_backups")
        assert hasattr(bk, "BackupInfo")
