"""Tests for infrastructure.visitor — visitor state tracking."""

from unittest.mock import patch

from infrastructure.visitor import VisitorRegistry, VisitorState

# ---------------------------------------------------------------------------
# VisitorState dataclass tests
# ---------------------------------------------------------------------------


class TestVisitorState:
    """Tests for the VisitorState dataclass."""

    def test_defaults(self):
        """VisitorState has correct defaults when only visitor_id is provided."""
        v = VisitorState(visitor_id="v1")

        assert v.visitor_id == "v1"
        assert v.display_name == "v1"  # Defaults to visitor_id
        assert v.position == {"x": 0.0, "y": 0.0, "z": 0.0}
        assert v.rotation == 0.0
        assert "T" in v.connected_at  # ISO format check

    def test_custom_values(self):
        """VisitorState accepts custom values for all fields."""
        v = VisitorState(
            visitor_id="v2",
            display_name="Alice",
            position={"x": 1.0, "y": 2.0, "z": 3.0},
            rotation=90.0,
            connected_at="2026-03-21T12:00:00Z",
        )

        assert v.visitor_id == "v2"
        assert v.display_name == "Alice"
        assert v.position == {"x": 1.0, "y": 2.0, "z": 3.0}
        assert v.rotation == 90.0
        assert v.connected_at == "2026-03-21T12:00:00Z"

    def test_display_name_defaults_to_visitor_id(self):
        """Empty display_name falls back to visitor_id."""
        v = VisitorState(visitor_id="charlie", display_name="")
        assert v.display_name == "charlie"

    def test_position_is_copied_not_shared(self):
        """Each VisitorState has its own position dict."""
        pos = {"x": 1.0, "y": 2.0, "z": 3.0}
        v1 = VisitorState(visitor_id="v1", position=pos)
        v2 = VisitorState(visitor_id="v2", position=pos)

        v1.position["x"] = 99.0
        assert v2.position["x"] == 1.0  # v2 unchanged


# ---------------------------------------------------------------------------
# VisitorRegistry singleton tests
# ---------------------------------------------------------------------------


class TestVisitorRegistrySingleton:
    """Tests for the VisitorRegistry singleton behavior."""

    def setup_method(self):
        """Clear registry before each test."""
        VisitorRegistry._instance = None

    def teardown_method(self):
        """Clean up after each test."""
        VisitorRegistry._instance = None

    def test_singleton_returns_same_instance(self):
        """Multiple calls return the same registry object."""
        r1 = VisitorRegistry()
        r2 = VisitorRegistry()
        assert r1 is r2

    def test_singleton_shares_state(self):
        """State is shared across all references to the singleton."""
        r1 = VisitorRegistry()
        r1.add("v1")

        r2 = VisitorRegistry()
        assert len(r2) == 1
        assert r2.get("v1") is not None


# ---------------------------------------------------------------------------
# VisitorRegistry.add tests
# ---------------------------------------------------------------------------


class TestVisitorRegistryAdd:
    """Tests for VisitorRegistry.add()."""

    def setup_method(self):
        """Clear registry before each test."""
        VisitorRegistry._instance = None
        self.registry = VisitorRegistry()

    def teardown_method(self):
        """Clean up after each test."""
        VisitorRegistry._instance = None

    def test_add_returns_visitor_state(self):
        """add() returns the created VisitorState."""
        result = self.registry.add("v1")
        assert isinstance(result, VisitorState)
        assert result.visitor_id == "v1"

    def test_add_with_display_name(self):
        """add() accepts a custom display name."""
        result = self.registry.add("v1", display_name="Alice")
        assert result.display_name == "Alice"

    def test_add_with_position(self):
        """add() accepts an initial position."""
        pos = {"x": 10.0, "y": 20.0, "z": 30.0}
        result = self.registry.add("v1", position=pos)
        assert result.position == pos

    def test_add_increases_count(self):
        """Each add increases the registry size."""
        assert len(self.registry) == 0
        self.registry.add("v1")
        assert len(self.registry) == 1
        self.registry.add("v2")
        assert len(self.registry) == 2


# ---------------------------------------------------------------------------
# VisitorRegistry.remove tests
# ---------------------------------------------------------------------------


class TestVisitorRegistryRemove:
    """Tests for VisitorRegistry.remove()."""

    def setup_method(self):
        """Clear registry and add test visitors."""
        VisitorRegistry._instance = None
        self.registry = VisitorRegistry()
        self.registry.add("v1")
        self.registry.add("v2")

    def teardown_method(self):
        """Clean up after each test."""
        VisitorRegistry._instance = None

    def test_remove_existing_returns_true(self):
        """Removing an existing visitor returns True."""
        result = self.registry.remove("v1")
        assert result is True
        assert len(self.registry) == 1

    def test_remove_nonexistent_returns_false(self):
        """Removing a non-existent visitor returns False."""
        result = self.registry.remove("unknown")
        assert result is False
        assert len(self.registry) == 2

    def test_removes_correct_visitor(self):
        """remove() only removes the specified visitor."""
        self.registry.remove("v1")
        assert self.registry.get("v1") is None
        assert self.registry.get("v2") is not None


# ---------------------------------------------------------------------------
# VisitorRegistry.update_position tests
# ---------------------------------------------------------------------------


class TestVisitorRegistryUpdatePosition:
    """Tests for VisitorRegistry.update_position()."""

    def setup_method(self):
        """Clear registry and add test visitor."""
        VisitorRegistry._instance = None
        self.registry = VisitorRegistry()
        self.registry.add("v1", position={"x": 0.0, "y": 0.0, "z": 0.0})

    def teardown_method(self):
        """Clean up after each test."""
        VisitorRegistry._instance = None

    def test_update_position_returns_true(self):
        """update_position returns True for existing visitor."""
        result = self.registry.update_position("v1", {"x": 1.0, "y": 2.0, "z": 3.0})
        assert result is True

    def test_update_position_returns_false_for_unknown(self):
        """update_position returns False for non-existent visitor."""
        result = self.registry.update_position("unknown", {"x": 1.0, "y": 2.0, "z": 3.0})
        assert result is False

    def test_update_position_changes_values(self):
        """update_position updates the stored position."""
        new_pos = {"x": 10.0, "y": 20.0, "z": 30.0}
        self.registry.update_position("v1", new_pos)

        visitor = self.registry.get("v1")
        assert visitor.position == new_pos

    def test_update_position_with_rotation(self):
        """update_position can also update rotation."""
        self.registry.update_position("v1", {"x": 1.0, "y": 0.0, "z": 0.0}, rotation=180.0)

        visitor = self.registry.get("v1")
        assert visitor.rotation == 180.0

    def test_update_position_without_rotation_preserves_it(self):
        """Calling update_position without rotation preserves existing rotation."""
        self.registry.update_position("v1", {"x": 1.0, "y": 0.0, "z": 0.0}, rotation=90.0)
        self.registry.update_position("v1", {"x": 2.0, "y": 0.0, "z": 0.0})

        visitor = self.registry.get("v1")
        assert visitor.rotation == 90.0


# ---------------------------------------------------------------------------
# VisitorRegistry.get tests
# ---------------------------------------------------------------------------


class TestVisitorRegistryGet:
    """Tests for VisitorRegistry.get()."""

    def setup_method(self):
        """Clear registry and add test visitor."""
        VisitorRegistry._instance = None
        self.registry = VisitorRegistry()
        self.registry.add("v1", display_name="Alice")

    def teardown_method(self):
        """Clean up after each test."""
        VisitorRegistry._instance = None

    def test_get_existing_returns_visitor(self):
        """get() returns VisitorState for existing visitor."""
        result = self.registry.get("v1")
        assert isinstance(result, VisitorState)
        assert result.visitor_id == "v1"
        assert result.display_name == "Alice"

    def test_get_nonexistent_returns_none(self):
        """get() returns None for non-existent visitor."""
        result = self.registry.get("unknown")
        assert result is None


# ---------------------------------------------------------------------------
# VisitorRegistry.get_all tests
# ---------------------------------------------------------------------------


class TestVisitorRegistryGetAll:
    """Tests for VisitorRegistry.get_all() — Matrix protocol format."""

    def setup_method(self):
        """Clear registry and add test visitors."""
        VisitorRegistry._instance = None
        self.registry = VisitorRegistry()
        self.registry.add("v1", display_name="Alice", position={"x": 1.0, "y": 2.0, "z": 3.0})
        self.registry.add("v2", display_name="Bob", position={"x": 4.0, "y": 5.0, "z": 6.0})

    def teardown_method(self):
        """Clean up after each test."""
        VisitorRegistry._instance = None

    def test_get_all_returns_list(self):
        """get_all() returns a list."""
        result = self.registry.get_all()
        assert isinstance(result, list)
        assert len(result) == 2

    def test_get_all_format_has_required_fields(self):
        """Each entry has type, visitor_id, data, and ts."""
        result = self.registry.get_all()

        for entry in result:
            assert "type" in entry
            assert "visitor_id" in entry
            assert "data" in entry
            assert "ts" in entry

    def test_get_all_type_is_visitor_state(self):
        """The type field is 'visitor_state'."""
        result = self.registry.get_all()
        assert all(entry["type"] == "visitor_state" for entry in result)

    def test_get_all_data_has_required_fields(self):
        """data dict contains display_name, position, rotation, connected_at."""
        result = self.registry.get_all()

        for entry in result:
            data = entry["data"]
            assert "display_name" in data
            assert "position" in data
            assert "rotation" in data
            assert "connected_at" in data

    def test_get_all_position_is_dict(self):
        """position within data is a dict with x, y, z."""
        result = self.registry.get_all()

        for entry in result:
            pos = entry["data"]["position"]
            assert isinstance(pos, dict)
            assert "x" in pos
            assert "y" in pos
            assert "z" in pos

    def test_get_all_ts_is_unix_timestamp(self):
        """ts is an integer Unix timestamp."""
        result = self.registry.get_all()

        for entry in result:
            assert isinstance(entry["ts"], int)
            assert entry["ts"] > 0

    @patch("infrastructure.visitor.time")
    def test_get_all_uses_current_time(self, mock_time):
        """ts is set from time.time()."""
        mock_time.time.return_value = 1742529600

        result = self.registry.get_all()
        assert all(entry["ts"] == 1742529600 for entry in result)

    def test_get_all_empty_registry(self):
        """get_all() returns empty list when no visitors."""
        self.registry.clear()
        result = self.registry.get_all()
        assert result == []


# ---------------------------------------------------------------------------
# VisitorRegistry.clear tests
# ---------------------------------------------------------------------------


class TestVisitorRegistryClear:
    """Tests for VisitorRegistry.clear()."""

    def setup_method(self):
        """Clear registry and add test visitors."""
        VisitorRegistry._instance = None
        self.registry = VisitorRegistry()
        self.registry.add("v1")
        self.registry.add("v2")
        self.registry.add("v3")

    def teardown_method(self):
        """Clean up after each test."""
        VisitorRegistry._instance = None

    def test_clear_removes_all_visitors(self):
        """clear() removes all visitors from the registry."""
        assert len(self.registry) == 3
        self.registry.clear()
        assert len(self.registry) == 0

    def test_clear_allows_readding(self):
        """Visitors can be re-added after clear()."""
        self.registry.clear()
        self.registry.add("v1")
        assert len(self.registry) == 1
