import json
import unittest
from unittest.mock import MagicMock, patch

from abstra_internals.controllers.codebase_events import CodebaseEventController
from abstra_internals.controllers.presence import (
    DEFAULT_HEARTBEAT_TTL,
    PresenceController,
    PresenceState,
)
from tests.fixtures import BaseTest


class PresenceControllerTest(unittest.TestCase):
    def setUp(self) -> None:
        PresenceController.reset_state()
        CodebaseEventController.listeners = []

    def tearDown(self) -> None:
        PresenceController.reset_state()
        CodebaseEventController.listeners = []

    def test_presence_join_update_leave(self) -> None:
        state_join = PresenceController.update_presence(
            "sess-a", "alice@example.com", "Alice", "main.py"
        )
        self.assertEqual(state_join.email, "alice@example.com")
        self.assertEqual(len(PresenceController.get_all_presence()), 1)

        state_update = PresenceController.update_presence(
            "sess-a", "alice@example.com", "Alice", "other.py"
        )
        self.assertEqual(state_update.current_file_path, "other.py")
        self.assertEqual(len(PresenceController.get_all_presence()), 1)

        removed = PresenceController.remove_presence("sess-a")
        self.assertTrue(removed)
        self.assertEqual(len(PresenceController.get_all_presence()), 0)

    def test_presence_remove_nonexistent(self) -> None:
        removed = PresenceController.remove_presence("sess-ghost")
        self.assertFalse(removed)

    def test_sweeper_expires_stale_presence(self) -> None:
        with patch(
            "abstra_internals.controllers.presence.time.time", return_value=1000.0
        ):
            PresenceController.update_presence(
                "sess-a", "alice@example.com", "Alice", None
            )
        with patch(
            "abstra_internals.controllers.presence.time.time", return_value=1100.0
        ):
            expired_presence = PresenceController.sweep_once(ttl=30.0)
        self.assertEqual(len(expired_presence), 1)
        self.assertEqual(expired_presence[0].session_id, "sess-a")
        self.assertEqual(len(PresenceController.get_all_presence()), 0)

    def test_sweeper_keeps_fresh_presence(self) -> None:
        with patch(
            "abstra_internals.controllers.presence.time.time", return_value=1000.0
        ):
            PresenceController.update_presence(
                "sess-a", "alice@example.com", "Alice", None
            )
        with patch(
            "abstra_internals.controllers.presence.time.time", return_value=1015.0
        ):
            expired_presence = PresenceController.sweep_once(ttl=DEFAULT_HEARTBEAT_TTL)
        self.assertEqual(expired_presence, [])
        self.assertEqual(len(PresenceController.get_all_presence()), 1)

    def test_broadcast_presence_join_and_update(self) -> None:
        listener = MagicMock()
        CodebaseEventController.listeners = [listener]

        PresenceController.update_presence(
            "sess-a", "alice@example.com", "Alice", "main.py"
        )
        PresenceController.update_presence(
            "sess-a", "alice@example.com", "Alice", "other.py"
        )

        self.assertEqual(listener.send.call_count, 2)
        first = json.loads(listener.send.call_args_list[0].args[0])
        second = json.loads(listener.send.call_args_list[1].args[0])
        self.assertEqual(first["event"], "presence_joined")
        self.assertEqual(second["event"], "presence_update")
        self.assertEqual(second["presence"]["currentFilePath"], "other.py")

    def test_broadcast_presence_leave(self) -> None:
        PresenceController.update_presence(
            "sess-a", "alice@example.com", "Alice", "main.py"
        )
        listener = MagicMock()
        CodebaseEventController.listeners = [listener]

        PresenceController.remove_presence("sess-a")

        self.assertEqual(listener.send.call_count, 1)
        payload = json.loads(listener.send.call_args.args[0])
        self.assertEqual(payload["event"], "presence_left")
        self.assertEqual(payload["presence"]["sessionId"], "sess-a")

    def test_presence_state_is_immutable(self) -> None:
        presence = PresenceState(
            session_id="s",
            email="a@a",
            name="A",
            current_file_path=None,
            last_heartbeat_at=0.0,
        )
        with self.assertRaises(Exception):
            presence.email = "b@b"  # type: ignore[misc]


class PresenceRoutesTest(BaseTest):
    def setUp(self) -> None:
        super().setUp()
        self.client = self.get_editor_flask_client()
        PresenceController.reset_state()
        CodebaseEventController.listeners = []

    def tearDown(self) -> None:
        PresenceController.reset_state()
        CodebaseEventController.listeners = []
        super().tearDown()

    def test_presence_routes(self) -> None:
        response = self.client.post(
            "/_editor/api/locks/presence/heartbeat",
            json={"sessionId": "sess-a", "currentFilePath": "main.py"},
        )
        self.assertEqual(response.status_code, 200)

        response = self.client.get("/_editor/api/locks/presence")
        self.assertEqual(response.status_code, 200)
        users = (response.json or {}).get("users", [])
        self.assertEqual(len(users), 1)
        self.assertEqual(users[0]["sessionId"], "sess-a")
        self.assertEqual(users[0]["currentFilePath"], "main.py")

        response = self.client.post(
            "/_editor/api/locks/presence/leave", json={"sessionId": "sess-a"}
        )
        self.assertEqual(response.status_code, 200)
        response = self.client.get("/_editor/api/locks/presence")
        self.assertEqual((response.json or {}).get("users"), [])

    def test_presence_heartbeat_validates_session(self) -> None:
        response = self.client.post("/_editor/api/locks/presence/heartbeat", json={})
        self.assertEqual(response.status_code, 400)


if __name__ == "__main__":
    unittest.main()
