"""Tests for GoalStateStore file I/O layer.

Covers requirements: GOAL-02, GOAL-03.

All six tests exercise GoalStateStore against pytest's tmp_path fixture so
no real filesystem state leaks between tests.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING

import pytest

from mcp_vm_blackbox.goal_state_store import GoalStateStore
from mcp_vm_blackbox.models import GoalState, GoalStateLifecycle

if TYPE_CHECKING:
    from pathlib import Path


def _make_goal(
    vm_name: str = "test-vm", scenario_name: str = "smoke", status: GoalStateLifecycle = "committed"
) -> GoalState:
    return GoalState(vm_name=vm_name, scenario_name=scenario_name, status=status)


def test_store_save_writes_json_file(tmp_path: Path) -> None:
    """Store.save() writes a committed GoalState as a JSON file at the expected path."""
    store = GoalStateStore(tmp_path)
    goal = _make_goal()
    store.save(goal)

    expected = tmp_path / "goal-states" / "test-vm" / "smoke.json"
    assert expected.exists(), f"Expected file not found: {expected}"
    data = json.loads(expected.read_text(encoding="utf-8"))
    assert data["vm_name"] == "test-vm"
    assert data["scenario_name"] == "smoke"


def test_store_load_reads_committed(tmp_path: Path) -> None:
    """Store.load() returns a GoalState from a committed JSON file on disk."""
    store = GoalStateStore(tmp_path)
    goal = _make_goal()
    store.save(goal)

    loaded = store.load("test-vm", "smoke")
    assert loaded is not None
    assert loaded.model_dump() == goal.model_dump()


def test_store_draft_creates_draft_file(tmp_path: Path) -> None:
    """Store.save_draft() writes a GoalState with status='draft' to a .draft.json file."""
    store = GoalStateStore(tmp_path)
    goal = _make_goal(status="draft")
    store.save_draft(goal)

    draft = tmp_path / "goal-states" / "test-vm" / "smoke.draft.json"
    assert draft.exists(), f"Expected draft file not found: {draft}"


def test_store_promote_renames_draft_to_committed(tmp_path: Path) -> None:
    """Store.promote_draft() renames .draft.json to .json and sets status='committed'."""
    store = GoalStateStore(tmp_path)
    goal = _make_goal(status="draft")
    store.save_draft(goal)

    promoted = store.promote_draft("test-vm", "smoke")

    committed = tmp_path / "goal-states" / "test-vm" / "smoke.json"
    draft = tmp_path / "goal-states" / "test-vm" / "smoke.draft.json"
    assert committed.exists(), "Committed file should exist after promote"
    assert not draft.exists(), "Draft file should be gone after promote"
    assert promoted.status == "committed"


def test_store_load_returns_none_for_missing(tmp_path: Path) -> None:
    """Store.load() returns None when no file exists for the given vm_name/scenario_name."""
    store = GoalStateStore(tmp_path)
    result = store.load("nonexistent-vm", "no-such-scenario")
    assert result is None


def test_atomic_write_no_torn_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """Store.save() uses an atomic write pattern (write tmp then rename) to avoid torn files."""
    store = GoalStateStore(tmp_path)

    # Save a known-good committed file first.
    good_goal = _make_goal(scenario_name="stable")
    store.save(good_goal)
    committed = tmp_path / "goal-states" / "test-vm" / "stable.json"
    original_content = committed.read_text(encoding="utf-8")

    # Monkeypatch json.dump to raise after the file descriptor is open.
    import mcp_vm_blackbox.goal_state_store as store_module

    original_dump = json.dump

    def _failing_dump(obj, fp, **kwargs):
        raise OSError("simulated write failure")

    monkeypatch.setattr(store_module.json, "dump", _failing_dump)

    with pytest.raises(OSError, match="simulated write failure"):
        store.save(_make_goal(scenario_name="stable"))

    monkeypatch.setattr(store_module.json, "dump", original_dump)

    # Committed file must be unchanged.
    assert committed.read_text(encoding="utf-8") == original_content

    # No .tmp files should remain.
    tmp_files = list((tmp_path / "goal-states" / "test-vm").glob("*.tmp"))
    assert tmp_files == [], f"Orphaned temp files found: {tmp_files}"
