"""Phase 3 Step 1 — parity between repo dict output and Pydantic DTOs.

These tests pin the contract that repo-hydrated dicts deserialize
cleanly into the REST DTOs.  If a repo-row shape drifts, the DTO test
fails before any endpoint rides on the stale contract.
"""
from __future__ import annotations

import pytest

from decnet.topology.config import TopologyConfig
from decnet.topology.generator import generate
from decnet.topology.persistence import hydrate, persist, transition_status
from decnet.topology.status import TopologyStatus
from decnet.web.db.factory import get_repository
from decnet.web.db.models import (
    DeckyRow,
    EdgeRow,
    LANRow,
    MutationEnqueueRequest,
    MutationRow,
    TopologyDetail,
    TopologyGenerateRequest,
    TopologyListResponse,
    TopologyStatusEventRow,
    TopologySummary,
)
from decnet.web.router.topology import topology_router


def _cfg() -> TopologyConfig:
    return TopologyConfig(
        name="dto-parity",
        depth=1,
        branching_factor=1,
        deckies_per_lan_min=1,
        deckies_per_lan_max=1,
        services_explicit=["ssh"],
        randomize_services=False,
        seed=0,
    )


@pytest.fixture
async def repo(tmp_path):
    r = get_repository(db_path=str(tmp_path / "dto.db"))
    await r.initialize()
    return r


def test_router_skeleton_mounted():
    """topology_router lives under /topologies and is import-safe."""
    assert topology_router.prefix == "/topologies"
    assert "topologies" in (topology_router.tags or [])


def test_generate_request_accepts_cli_shape():
    """TopologyGenerateRequest mirrors the CLI flags."""
    req = TopologyGenerateRequest(
        name="n",
        depth=2,
        branching_factor=2,
        deckies_per_lan_min=1,
        deckies_per_lan_max=3,
        services_explicit=["ssh", "ftp"],
        randomize_services=False,
        seed=7,
    )
    assert req.depth == 2
    assert req.services_explicit == ["ssh", "ftp"]


def test_mutation_request_rejects_unknown_op():
    """Literal guard is what gives the frontend a free 422 contract."""
    with pytest.raises(ValueError):
        MutationEnqueueRequest(op="teleport_lan", payload={})


@pytest.mark.anyio
async def test_summary_accepts_repo_topology_row(repo):
    plan = generate(_cfg())
    tid = await persist(repo, plan)
    summary = await repo.get_topology(tid)
    assert summary.id == tid
    assert summary.version == 1
    # Defaults surface cleanly on a fresh topology.
    assert summary.needs_resync is False
    assert summary.target_host_uuid is None


@pytest.mark.anyio
async def test_summary_surfaces_needs_resync_flag(repo):
    """When the heartbeat handler flags a topology for resync, the API
    list/detail views must expose it so operators can debug without
    shelling into the DB."""
    plan = generate(_cfg())
    tid = await persist(repo, plan)
    await repo.set_topology_resync(tid, True)
    summary = await repo.get_topology(tid)
    assert summary.needs_resync is True


@pytest.mark.anyio
async def test_detail_accepts_hydrated_shape(repo):
    plan = generate(_cfg())
    tid = await persist(repo, plan)
    hydrated = await hydrate(repo, tid)
    detail = TopologyDetail(
        topology=TopologySummary(**hydrated["topology"]),
        lans=[LANRow(**l) for l in hydrated["lans"]],
        deckies=[DeckyRow(**d) for d in hydrated["deckies"]],
        edges=[EdgeRow(**e) for e in hydrated["edges"]],
    )
    assert detail.topology.id == tid
    assert len(detail.lans) == len(hydrated["lans"])
    assert len(detail.deckies) == len(hydrated["deckies"])


@pytest.mark.anyio
async def test_mutation_row_accepts_repo_row(repo):
    plan = generate(_cfg())
    tid = await persist(repo, plan)
    mid = await repo.enqueue_topology_mutation(
        tid, "add_lan", {"name": "LAN-X"}
    )
    rows = await repo.list_topology_mutations(tid)
    assert rows and rows[0]["id"] == mid
    m = MutationRow(**rows[0])
    assert m.op == "add_lan"
    assert m.payload == {"name": "LAN-X"}


@pytest.mark.anyio
async def test_status_event_row_accepts_repo_row(repo):
    plan = generate(_cfg())
    tid = await persist(repo, plan)
    await transition_status(repo, tid, TopologyStatus.DEPLOYING)
    events = await repo.list_topology_status_events(tid)
    assert events
    TopologyStatusEventRow(**events[0])


def test_list_response_envelope_shape():
    resp = TopologyListResponse(total=0, limit=50, offset=0, data=[])
    assert resp.total == 0
    assert resp.data == []
