"""
Tests for Cloud Team Memory (kodara/cloud/team_cloud.py).

All Supabase calls are mocked — no real network traffic.
"""

from __future__ import annotations

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

import pytest

from kodara.cloud.team_cloud import (
    TeamInfo,
    create_team,
    join_team,
    push_to_team,
    pull_from_team,
    get_team_info,
)
from kodara.memory.store import KodaraIndex, INDEX_VERSION


# ── Fixtures ──────────────────────────────────────────────────────────────────

def _make_index(tmp_path: Path) -> KodaraIndex:
    return KodaraIndex(
        repo_id="local/myproject-abcd1234",
        display_name="myproject",
        source_root=str(tmp_path),
        indexed_at=time.time(),
        index_version=INDEX_VERSION,
        file_count=83,
        language_counts={"python": 83},
        modules={"main.py": {"path": "main.py", "imports": []}},
        graph_edges=[],
        file_hashes={"main.py": "deadbeef"},
    )


def _mock_client():
    """Build a fully-stubbed supabase client."""
    client = MagicMock()
    return client


# ── create_team ───────────────────────────────────────────────────────────────

class TestCreateTeam:
    def test_creates_team_and_adds_member(self, tmp_path):
        """create_team inserts into teams + team_members tables."""
        client = _mock_client()
        team_row = {
            "id": "team-uuid-1",
            "name": "alpha",
            "invite_code": "ab12cd34",
            "owner_id": "user-1",
        }
        client.table.return_value.insert.return_value.execute.return_value.data = [team_row]
        client.table.return_value.upsert.return_value.execute.return_value.data = [{}]

        with patch("kodara.cloud.team_cloud.get_client", return_value=client), \
             patch("kodara.cloud.team_cloud._save_team_id") as mock_save, \
             patch("kodara.cloud.team_cloud._generate_invite_code", return_value="ab12cd34"):

            info = create_team("alpha", "user-1")

        assert isinstance(info, TeamInfo)
        assert info.id == "team-uuid-1"
        assert info.name == "alpha"
        assert info.member_count == 1
        mock_save.assert_called_once_with("team-uuid-1")

    def test_returns_team_info_with_invite_code(self, tmp_path):
        """create_team returns TeamInfo with the generated invite_code."""
        client = _mock_client()
        team_row = {
            "id": "team-uuid-2",
            "name": "beta",
            "invite_code": "zz99yy88",
            "owner_id": "user-2",
        }
        client.table.return_value.insert.return_value.execute.return_value.data = [team_row]
        client.table.return_value.upsert.return_value.execute.return_value.data = [{}]

        with patch("kodara.cloud.team_cloud.get_client", return_value=client), \
             patch("kodara.cloud.team_cloud._save_team_id"), \
             patch("kodara.cloud.team_cloud._generate_invite_code", return_value="zz99yy88"):

            info = create_team("beta", "user-2")

        assert info.invite_code == "zz99yy88"

    def test_raises_on_empty_response(self):
        """create_team raises RuntimeError when Supabase returns no data."""
        client = _mock_client()
        client.table.return_value.insert.return_value.execute.return_value.data = []

        with patch("kodara.cloud.team_cloud.get_client", return_value=client), \
             patch("kodara.cloud.team_cloud._save_team_id"):

            with pytest.raises(RuntimeError, match="Failed to create team"):
                create_team("fail-team", "user-x")


# ── join_team ─────────────────────────────────────────────────────────────────

class TestJoinTeam:
    def test_joins_team_and_adds_member(self):
        """join_team finds team by invite_code and upserts member row."""
        client = _mock_client()
        team_row = {
            "id": "team-uuid-3",
            "name": "gamma",
            "invite_code": "invite01",
        }

        # Chain: .select().eq().execute()
        select_mock = MagicMock()
        select_mock.execute.return_value.data = [team_row]
        eq_mock = MagicMock()
        eq_mock.execute = select_mock.execute
        select_mock.eq = MagicMock(return_value=eq_mock)

        upsert_mock = MagicMock()
        upsert_mock.execute.return_value.data = [{}]

        count_mock = MagicMock()
        count_mock.execute.return_value.data = [{"user_id": "u1"}, {"user_id": "u2"}]
        count_select = MagicMock()
        count_select.eq = MagicMock(return_value=count_mock)

        def table_side_effect(name):
            t = MagicMock()
            if name == "teams":
                t.select.return_value = select_mock
            elif name == "team_members":
                t.upsert.return_value = upsert_mock
                t.select.return_value = count_select
            return t

        client.table.side_effect = table_side_effect

        with patch("kodara.cloud.team_cloud.get_client", return_value=client), \
             patch("kodara.cloud.team_cloud._save_team_id") as mock_save:

            info = join_team("invite01", "user-3")

        assert info.id == "team-uuid-3"
        assert info.name == "gamma"
        mock_save.assert_called_once_with("team-uuid-3")

    def test_invalid_invite_code_raises(self):
        """join_team raises ValueError for unknown invite code."""
        client = _mock_client()

        select_mock = MagicMock()
        select_mock.execute.return_value.data = []
        eq_mock = MagicMock()
        eq_mock.execute = select_mock.execute
        select_mock.eq = MagicMock(return_value=eq_mock)

        def table_side_effect(name):
            t = MagicMock()
            t.select.return_value = select_mock
            return t

        client.table.side_effect = table_side_effect

        with patch("kodara.cloud.team_cloud.get_client", return_value=client):
            with pytest.raises(ValueError, match="No team found with invite code"):
                join_team("badcode", "user-x")


# ── push_to_team ──────────────────────────────────────────────────────────────

class TestPushToTeam:
    def test_upserts_team_memory(self, tmp_path):
        """push_to_team upserts project + team_memory rows."""
        client = _mock_client()
        project_row = {"id": "proj-uuid-1", "name": "myproject"}

        upsert_project = MagicMock()
        upsert_project.execute.return_value.data = [project_row]

        upsert_memory = MagicMock()
        upsert_memory.execute.return_value.data = [{}]

        call_count = {"projects": 0, "team_memory": 0}

        def table_side_effect(name):
            t = MagicMock()
            if name == "projects":
                t.upsert.return_value = upsert_project
            elif name == "team_memory":
                t.upsert.return_value = upsert_memory
            return t

        client.table.side_effect = table_side_effect

        index = _make_index(tmp_path)

        with patch("kodara.cloud.team_cloud.get_client", return_value=client):
            push_to_team("team-uuid-1", index, "user-1")

        # Both tables should have been touched
        table_names = [c.args[0] for c in client.table.call_args_list]
        assert "projects" in table_names
        assert "team_memory" in table_names

    def test_raises_on_project_upsert_failure(self, tmp_path):
        """push_to_team raises RuntimeError if project upsert returns no data."""
        client = _mock_client()

        upsert_fail = MagicMock()
        upsert_fail.execute.return_value.data = []

        def table_side_effect(name):
            t = MagicMock()
            t.upsert.return_value = upsert_fail
            return t

        client.table.side_effect = table_side_effect
        index = _make_index(tmp_path)

        with patch("kodara.cloud.team_cloud.get_client", return_value=client):
            with pytest.raises(RuntimeError, match="Failed to upsert project"):
                push_to_team("team-uuid-1", index, "user-1")


# ── pull_from_team ────────────────────────────────────────────────────────────

class TestPullFromTeam:
    def test_loads_and_saves_index(self, tmp_path):
        """pull_from_team fetches index_data and saves it via MemoryStore."""
        from dataclasses import asdict
        index = _make_index(tmp_path)
        index_data = asdict(index)

        client = _mock_client()
        memory_row = {
            "team_id": "team-uuid-1",
            "project_id": "proj-uuid-1",
            "index_data": index_data,
            "updated_at": "2026-01-01T00:00:00Z",
            "projects": {
                "name": tmp_path.name,
                "repo_id": index.repo_id,
                "source_root": str(tmp_path),
                "file_count": 83,
            },
        }

        select_mock = MagicMock()
        eq_mock = MagicMock()
        eq_mock.execute.return_value.data = [memory_row]
        select_mock.eq = MagicMock(return_value=eq_mock)

        def table_side_effect(name):
            t = MagicMock()
            t.select.return_value = select_mock
            return t

        client.table.side_effect = table_side_effect

        with patch("kodara.cloud.team_cloud.get_client", return_value=client), \
             patch("kodara.memory.store.MemoryStore.repo_id_from_path",
                   return_value=index.repo_id):

            pulled = pull_from_team("team-uuid-1", tmp_path)

        assert isinstance(pulled, KodaraIndex)
        assert pulled.file_count == 83
        assert pulled.display_name == "myproject"

    def test_raises_if_no_team_memory(self, tmp_path):
        """pull_from_team raises ValueError when team has no memory records."""
        client = _mock_client()

        select_mock = MagicMock()
        eq_mock = MagicMock()
        eq_mock.execute.return_value.data = []
        select_mock.eq = MagicMock(return_value=eq_mock)

        def table_side_effect(name):
            t = MagicMock()
            t.select.return_value = select_mock
            return t

        client.table.side_effect = table_side_effect

        with patch("kodara.cloud.team_cloud.get_client", return_value=client):
            with pytest.raises(ValueError, match="No team memory found"):
                pull_from_team("team-uuid-empty", tmp_path)


# ── get_team_info ─────────────────────────────────────────────────────────────

class TestGetTeamInfo:
    def test_returns_list_of_teams(self):
        """get_team_info returns TeamInfo list for all user teams."""
        client = _mock_client()

        membership_rows = [{"team_id": "t1"}, {"team_id": "t2"}]
        team_rows = [
            {"id": "t1", "name": "Alpha", "invite_code": "aaa11111"},
            {"id": "t2", "name": "Beta",  "invite_code": "bbb22222"},
        ]

        memberships_mock = MagicMock()
        memberships_mock.execute.return_value.data = membership_rows
        eq_memberships = MagicMock()
        eq_memberships.execute = memberships_mock.execute
        memberships_select = MagicMock()
        memberships_select.eq = MagicMock(return_value=eq_memberships)

        teams_mock = MagicMock()
        teams_mock.execute.return_value.data = team_rows
        in_mock = MagicMock()
        in_mock.execute = teams_mock.execute
        teams_select = MagicMock()
        teams_select.in_ = MagicMock(return_value=in_mock)

        count_mock = MagicMock()
        count_mock.execute.return_value.data = [{"user_id": "u1"}, {"user_id": "u2"}]
        count_select = MagicMock()
        count_eq = MagicMock()
        count_eq.execute = count_mock.execute
        count_select.eq = MagicMock(return_value=count_eq)

        call_index = {"n": 0}

        def table_side_effect(name):
            t = MagicMock()
            if name == "team_members":
                # First call: memberships; subsequent calls: count
                call_index["n"] += 1
                if call_index["n"] == 1:
                    t.select.return_value = memberships_select
                else:
                    t.select.return_value = count_select
            elif name == "teams":
                t.select.return_value = teams_select
            return t

        client.table.side_effect = table_side_effect

        with patch("kodara.cloud.team_cloud.get_client", return_value=client):
            infos = get_team_info("user-1")

        assert len(infos) == 2
        names = {i.name for i in infos}
        assert names == {"Alpha", "Beta"}

    def test_returns_empty_list_if_no_teams(self):
        """get_team_info returns [] when user has no team memberships."""
        client = _mock_client()

        memberships_mock = MagicMock()
        memberships_mock.execute.return_value.data = []
        eq_mock = MagicMock()
        eq_mock.execute = memberships_mock.execute
        select_mock = MagicMock()
        select_mock.eq = MagicMock(return_value=eq_mock)

        def table_side_effect(name):
            t = MagicMock()
            t.select.return_value = select_mock
            return t

        client.table.side_effect = table_side_effect

        with patch("kodara.cloud.team_cloud.get_client", return_value=client):
            infos = get_team_info("user-lonely")

        assert infos == []


# ── Regression: existing team CLI commands not broken ─────────────────────────

class TestExistingTeamCommands:
    """Smoke test — existing team group commands still importable and callable."""

    def test_team_group_exists(self):
        """The CLI 'team' group and its sub-commands are still importable."""
        from click.testing import CliRunner
        from kodara.cli.main import main

        runner = CliRunner()
        result = runner.invoke(main, ["team", "--help"])
        assert result.exit_code == 0
        assert "config" in result.output or "push" in result.output or "Share" in result.output.lower()

    def test_team_config_command_exists(self):
        """'kodara team config' sub-command is still present."""
        from click.testing import CliRunner
        from kodara.cli.main import main

        runner = CliRunner()
        result = runner.invoke(main, ["team", "config", "--help"])
        assert result.exit_code == 0

    def test_team_push_command_exists(self):
        """'kodara team push' sub-command is still present."""
        from click.testing import CliRunner
        from kodara.cli.main import main

        runner = CliRunner()
        result = runner.invoke(main, ["team", "push", "--help"])
        assert result.exit_code == 0

    def test_team_pull_command_exists(self):
        """'kodara team pull' sub-command is still present."""
        from click.testing import CliRunner
        from kodara.cli.main import main

        runner = CliRunner()
        result = runner.invoke(main, ["team", "pull", "--help"])
        assert result.exit_code == 0

    def test_team_status_command_exists(self):
        """'kodara team status' sub-command is still present."""
        from click.testing import CliRunner
        from kodara.cli.main import main

        runner = CliRunner()
        result = runner.invoke(main, ["team", "status", "--help"])
        assert result.exit_code == 0

    def test_new_cloud_commands_exist(self):
        """New cloud sub-commands are registered under 'team'."""
        from click.testing import CliRunner
        from kodara.cli.main import main

        runner = CliRunner()
        for cmd in ("cloud-create", "cloud-join", "cloud-push", "cloud-pull", "cloud-status"):
            result = runner.invoke(main, ["team", cmd, "--help"])
            assert result.exit_code == 0, (
                f"'kodara team {cmd} --help' exited with {result.exit_code}:\n{result.output}"
            )
