"""Tests for JobStore async facade over CoordinationDB.

All tests use pytest's tmp_path fixture for isolation.
asyncio_mode = "auto" (configured in pyproject.toml) — no decorator needed.
"""

from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING

import pytest

from mcp_vm_blackbox.coordination_db import _reset_db_instance_for_testing
from mcp_vm_blackbox.job_store import JobStore

if TYPE_CHECKING:
    from collections.abc import Generator
    from pathlib import Path


@pytest.fixture(autouse=True)
def reset_db() -> Generator[None, None, None]:
    """Reset the CoordinationDB singleton between tests for isolation."""
    _reset_db_instance_for_testing()
    yield
    _reset_db_instance_for_testing()


async def test_start_creates_file(tmp_path: Path) -> None:
    """start() does not write a JSON file (DR-3 — only terminal states write JSON)."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "install", "agent-1", "begin install")
    # JSON file is NOT written on start — only on terminal state transitions
    path = tmp_path / "jobs" / "my-vm" / f"{job_id}.json"
    assert not path.exists(), "JSON should not be written on start (DR-3)"


async def test_start_returns_job_id(tmp_path: Path) -> None:
    """start() returns a non-empty string job ID."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "install", "agent-1", "begin install")
    assert isinstance(job_id, str)
    assert len(job_id) == 12


async def test_load_round_trip(tmp_path: Path) -> None:
    """start() + load() round-trips all fields correctly."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "smoke", "agent-1", "kick off")
    record = await store.load(job_id, "my-vm")

    assert record.job_id == job_id
    assert record.vm_name == "my-vm"
    assert record.scenario_name == "smoke"
    assert record.created_by == "agent-1"
    assert record.last_agent_id == "agent-1"
    assert record.status == "active"
    assert len(record.steps) == 1
    assert record.steps[0].agent_id == "agent-1"
    assert record.steps[0].action == "kick off"


async def test_append_step(tmp_path: Path) -> None:
    """append_step() adds a step to the job's step log."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "smoke", "agent-1", "step one")
    await store.append_step(job_id, "my-vm", "agent-2", "step two", outcome="success")

    record = await store.load(job_id, "my-vm")
    assert len(record.steps) == 2
    assert record.steps[1].agent_id == "agent-2"
    assert record.steps[1].action == "step two"
    assert record.steps[1].outcome == "success"


async def test_set_status(tmp_path: Path) -> None:
    """set_status() changes status and advances updated_at."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "smoke", "agent-1", "init")
    original = await store.load(job_id, "my-vm")

    # Small delay to ensure timestamp advances
    await asyncio.sleep(0.01)
    await store.set_status(job_id, "my-vm", "paused")

    updated = await store.load(job_id, "my-vm")
    assert updated.status == "paused"
    assert updated.updated_at >= original.updated_at


async def test_set_status_writes_json_on_terminal(tmp_path: Path) -> None:
    """set_status() writes JSON evidence file on terminal state transitions (DR-3)."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "smoke", "agent-1", "init")

    path = tmp_path / "jobs" / "my-vm" / f"{job_id}.json"
    assert not path.exists(), "JSON should not exist before terminal transition"

    await store.set_status(job_id, "my-vm", "completed")
    assert path.exists(), "JSON should be written on completed (terminal state)"


async def test_set_status_no_json_on_non_terminal(tmp_path: Path) -> None:
    """set_status() does not write JSON for non-terminal transitions (DR-3)."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "smoke", "agent-1", "init")
    await store.set_status(job_id, "my-vm", "paused")

    # paused IS a terminal state that triggers JSON — reset and test active→active
    # active is already active, so use a fresh job that goes paused→active
    job_id2 = await store.start("my-vm", "smoke2", "agent-1", "init")
    await store.set_status(job_id2, "my-vm", "paused")
    await store.set_status(job_id2, "my-vm", "active")
    path2 = tmp_path / "jobs" / "my-vm" / f"{job_id2}.json"
    # The JSON exists from the paused transition; active does not re-write
    assert path2.exists()


async def test_set_status_invalid_transition(tmp_path: Path) -> None:
    """set_status() raises ValueError on illegal transitions."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "smoke", "agent-1", "init")
    await store.set_status(job_id, "my-vm", "completed")

    with pytest.raises(ValueError, match="Invalid status transition"):
        await store.set_status(job_id, "my-vm", "active")


async def test_list_by_vm(tmp_path: Path) -> None:
    """list_by_vm() returns only jobs for the specified VM."""
    store = JobStore(tmp_path)
    await store.start("vm-a", "smoke", "agent-1", "init")
    await store.start("vm-a", "install", "agent-1", "init")
    await store.start("vm-b", "smoke", "agent-1", "init")

    results = await store.list_by_vm("vm-a")
    assert len(results) == 2
    assert all(r.vm_name == "vm-a" for r in results)


async def test_list_by_status(tmp_path: Path) -> None:
    """list_by_status() returns only jobs matching the given status."""
    store = JobStore(tmp_path)
    id1 = await store.start("my-vm", "smoke", "agent-1", "init")
    await store.start("my-vm", "install", "agent-1", "init")
    await store.set_status(id1, "my-vm", "paused")

    paused = await store.list_by_status("paused")
    assert len(paused) == 1
    assert paused[0].job_id == id1

    active = await store.list_by_status("active")
    assert len(active) == 1


async def test_list_by_vm_empty(tmp_path: Path) -> None:
    """list_by_vm() returns an empty list for an unknown VM."""
    store = JobStore(tmp_path)
    assert await store.list_by_vm("nonexistent") == []


async def test_persistence_survives_new_instance(tmp_path: Path) -> None:
    """A new JobStore against the same path can load jobs created by the first.

    Both instances share the same SQLite db via get_db() singleton.
    """
    store1 = JobStore(tmp_path)
    job_id = await store1.start("my-vm", "smoke", "agent-1", "init")

    store2 = JobStore(tmp_path)
    record = await store2.load(job_id, "my-vm")
    assert record.job_id == job_id
    assert record.vm_name == "my-vm"


async def test_gitignore_created(tmp_path: Path) -> None:
    """.gitignore is written in jobs/ when a terminal-state JSON is written."""
    store = JobStore(tmp_path)
    job_id = await store.start("my-vm", "smoke", "agent-1", "init")
    await store.set_status(job_id, "my-vm", "completed")
    gitignore = tmp_path / "jobs" / ".gitignore"
    assert gitignore.exists()


async def test_load_missing_raises(tmp_path: Path) -> None:
    """load() raises FileNotFoundError for a non-existent job."""
    store = JobStore(tmp_path)
    with pytest.raises(FileNotFoundError):
        await store.load("nonexistent", "no-vm")
