"""Behavior + thread-safety tests for :class:`mgf.vm.core.monitor.state.StateStore`.

These assert the two load-bearing invariants of the store:

* every read returns a *frozen* (independent) copy, so a caller can
  iterate / mutate the result without corrupting the registry; and
* concurrent writers from many threads leave the store internally
  consistent (the single :class:`threading.RLock` serialises every
  public method).
"""

from __future__ import annotations

import threading

import pytest

from mgf.vm.core.domain import VMHandle, VMState, VMStatus
from mgf.vm.core.monitor.state import DomainHandleSource, StateStore


def _handle(name: str, status: VMStatus = VMStatus.STOPPED, uuid: str = "") -> VMHandle:
    return VMHandle(name=name, uuid=uuid or f"uuid-{name}", state=status)


class _FakeSource:
    """A minimal :class:`DomainHandleSource` for seeding tests."""

    def __init__(self, handles: list[VMHandle]) -> None:
        self._handles = handles

    def list_domains(self) -> list[VMHandle]:
        return list(self._handles)


# ---------------------------------------------------------------------------
# Seeding
# ---------------------------------------------------------------------------


class TestSeed:
    def test_seed_loads_every_handle(self) -> None:
        store = StateStore()
        count = store.seed(_FakeSource([_handle("a"), _handle("b")]))
        assert count == 2
        assert store.contains("a")
        assert store.contains("b")
        assert len(store) == 2

    def test_seed_records_seeded_at(self) -> None:
        store = StateStore()
        assert store.seeded_at is None
        store.seed(_FakeSource([_handle("a")]))
        assert store.seeded_at is not None

    def test_seed_copies_handle_state_into_status(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle("a", VMStatus.RUNNING)]))
        state = store.get("a")
        assert state is not None
        assert state.status is VMStatus.RUNNING

    def test_reseed_replaces_entire_set(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle("old")]))
        store.seed(_FakeSource([_handle("new")]))
        assert not store.contains("old")
        assert store.contains("new")

    def test_fake_source_satisfies_protocol(self) -> None:
        assert isinstance(_FakeSource([]), DomainHandleSource)


# ---------------------------------------------------------------------------
# Frozen-return protocol — the core thread-safety contract for readers
# ---------------------------------------------------------------------------


class TestFrozenReturns:
    def test_get_returns_independent_copy(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle("a", VMStatus.STOPPED)]))

        snapshot = store.get("a")
        assert snapshot is not None
        # Mutating the returned (mutable) VMState must NOT leak into the store.
        snapshot.status = VMStatus.RUNNING
        snapshot.ip = "10.0.0.1"

        fresh = store.get("a")
        assert fresh is not None
        assert fresh.status is VMStatus.STOPPED
        assert fresh.ip is None

    def test_get_returns_distinct_objects_each_call(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle("a")]))
        assert store.get("a") is not store.get("a")

    def test_get_missing_returns_none(self) -> None:
        assert StateStore().get("nope") is None

    def test_list_all_entries_are_independent_copies(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle("a"), _handle("b")]))

        for state in store.list_all():
            state.status = VMStatus.CRASHED

        assert all(s.status is not VMStatus.CRASHED for s in store.list_all())

    def test_list_all_is_a_point_in_time_snapshot(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle("a")]))
        snapshot = store.list_all()
        store.insert(_handle("b"))
        # The previously-captured list is unaffected by later mutation.
        assert [s.handle.name for s in snapshot] == ["a"]

    def test_iter_yields_frozen_snapshot(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle("a"), _handle("b")]))
        names = {s.handle.name for s in store}
        assert names == {"a", "b"}

    def test_insert_returns_independent_copy(self) -> None:
        store = StateStore()
        returned = store.insert(_handle("a", VMStatus.RUNNING))
        returned.status = VMStatus.STOPPED
        live = store.get("a")
        assert live is not None
        assert live.status is VMStatus.RUNNING

    def test_update_returns_independent_copy(self) -> None:
        store = StateStore()
        store.insert(_handle("a", VMStatus.STOPPED))
        returned = store.update("a", status=VMStatus.RUNNING)
        assert returned is not None
        returned.ip = "leak"
        live = store.get("a")
        assert live is not None
        assert live.ip is None


# ---------------------------------------------------------------------------
# Write semantics
# ---------------------------------------------------------------------------


class TestWrites:
    def test_insert_then_get(self) -> None:
        store = StateStore()
        store.insert(_handle("a", VMStatus.RUNNING))
        state = store.get("a")
        assert state is not None
        assert state.status is VMStatus.RUNNING

    def test_duplicate_insert_records_previous_status_and_keeps_ip(self) -> None:
        store = StateStore()
        store.insert(_handle("a", VMStatus.STOPPED))
        store.update("a", ip="10.0.0.5")
        re_inserted = store.insert(_handle("a", VMStatus.RUNNING))
        assert re_inserted.status is VMStatus.RUNNING
        assert re_inserted.previous_status is VMStatus.STOPPED
        assert re_inserted.ip == "10.0.0.5"

    def test_update_only_changes_supplied_fields(self) -> None:
        store = StateStore()
        store.insert(_handle("a", VMStatus.RUNNING))
        store.update("a", ip="10.0.0.9")
        store.update("a", uptime_seconds=42)
        state = store.get("a")
        assert state is not None
        assert state.ip == "10.0.0.9"
        assert state.uptime_seconds == 42
        assert state.status is VMStatus.RUNNING

    def test_update_records_transition_metadata_on_status_change(self) -> None:
        store = StateStore()
        store.insert(_handle("a", VMStatus.STOPPED))
        updated = store.update("a", status=VMStatus.RUNNING)
        assert updated is not None
        assert updated.previous_status is VMStatus.STOPPED
        assert updated.last_state_change is not None

    def test_update_without_status_change_preserves_previous_status(self) -> None:
        store = StateStore()
        store.insert(_handle("a", VMStatus.RUNNING))
        first = store.update("a", status=VMStatus.STOPPED)
        assert first is not None
        marker = first.last_state_change
        # No status change this time → last_state_change must not move.
        second = store.update("a", ip="1.2.3.4")
        assert second is not None
        assert second.last_state_change == marker
        assert second.previous_status is VMStatus.RUNNING

    def test_update_unknown_returns_none(self) -> None:
        assert StateStore().update("ghost", status=VMStatus.RUNNING) is None

    def test_remove_existing_returns_true(self) -> None:
        store = StateStore()
        store.insert(_handle("a"))
        assert store.remove("a") is True
        assert not store.contains("a")

    def test_remove_missing_returns_false(self) -> None:
        assert StateStore().remove("a") is False

    def test_rename_rekeys_and_rebuilds_handle(self) -> None:
        store = StateStore()
        store.insert(_handle("old", VMStatus.RUNNING, uuid="u-1"))
        store.update("old", ip="10.0.0.2")
        renamed = store.rename("old", "new")
        assert renamed is not None
        assert renamed.handle.name == "new"
        assert renamed.handle.uuid == "u-1"
        assert renamed.ip == "10.0.0.2"
        assert not store.contains("old")
        assert store.contains("new")

    def test_rename_missing_returns_none(self) -> None:
        assert StateStore().rename("nope", "new") is None

    def test_clear_empties_store_and_resets_seeded_at(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle("a")]))
        store.clear()
        assert len(store) == 0
        assert store.seeded_at is None


# ---------------------------------------------------------------------------
# Concurrency — the RLock must serialise every public method
# ---------------------------------------------------------------------------


class TestThreadSafety:
    def test_concurrent_inserts_are_all_recorded(self) -> None:
        store = StateStore()
        n = 200
        barrier = threading.Barrier(8)
        errors: list[BaseException] = []

        def worker(start: int) -> None:
            try:
                barrier.wait()
                for i in range(start, start + 25):
                    store.insert(_handle(f"vm-{i}"))
            except BaseException as exc:
                errors.append(exc)

        threads = [threading.Thread(target=worker, args=(s,)) for s in range(0, n, 25)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors
        assert len(store) == n
        assert {s.handle.name for s in store.list_all()} == {f"vm-{i}" for i in range(n)}

    def test_readers_and_writers_interleave_without_corruption(self) -> None:
        store = StateStore()
        store.seed(_FakeSource([_handle(f"vm-{i}") for i in range(20)]))
        stop = threading.Event()
        errors: list[BaseException] = []

        def reader() -> None:
            try:
                while not stop.is_set():
                    # Iterating a frozen snapshot must never raise even while
                    # writers mutate the underlying dict concurrently.
                    for state in store.list_all():
                        assert isinstance(state, VMState)
            except BaseException as exc:
                errors.append(exc)

        def writer() -> None:
            try:
                for _ in range(500):
                    store.update("vm-0", status=VMStatus.RUNNING)
                    store.update("vm-0", status=VMStatus.STOPPED)
            except BaseException as exc:
                errors.append(exc)

        readers = [threading.Thread(target=reader) for _ in range(4)]
        writers = [threading.Thread(target=writer) for _ in range(4)]
        for t in readers + writers:
            t.start()
        for t in writers:
            t.join()
        stop.set()
        for t in readers:
            t.join()

        assert not errors

    def test_concurrent_insert_and_remove_converges(self) -> None:
        store = StateStore()
        errors: list[BaseException] = []

        def churner(name: str) -> None:
            try:
                for _ in range(300):
                    store.insert(_handle(name))
                    store.remove(name)
            except BaseException as exc:
                errors.append(exc)

        threads = [threading.Thread(target=churner, args=(f"vm-{i}",)) for i in range(6)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors
        # Every churner ends on remove(), so the store must be empty.
        assert len(store) == 0


@pytest.mark.parametrize(
    "status",
    [VMStatus.RUNNING, VMStatus.STOPPED, VMStatus.PAUSED],
)
def test_seed_preserves_each_status(status: VMStatus) -> None:
    store = StateStore()
    store.seed(_FakeSource([_handle("a", status)]))
    state = store.get("a")
    assert state is not None
    assert state.status is status
