"""Tests for Phase 76: Cross-Swarm Dependency Tracking (25+ tests)."""

import json
from pathlib import Path

import pytest

import brynq.server as srv
import brynq.cross_swarm_deps as csd


@pytest.fixture(autouse=True)
def isolate(tmp_path, monkeypatch):
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(csd, "CROSS_DEPS_DIR", tmp_path / "cross_deps")
    (tmp_path / "cross_deps").mkdir(parents=True, exist_ok=True)


# ── declare_dependency ───────────────────────────────────────────────────


class TestDeclareDependency:
    def test_basic_declaration(self):
        result = csd.declare_dependency("swarm-A", "swarm-B", "A waits for B")
        assert result["ok"] is True
        assert result["dependency"]["swarm_id"] == "swarm-A"
        assert result["dependency"]["depends_on"] == "swarm-B"

    def test_description_stored(self):
        result = csd.declare_dependency("A", "B", "need data from B")
        assert result["dependency"]["description"] == "need data from B"

    def test_self_dependency_rejected(self):
        result = csd.declare_dependency("X", "X")
        assert result["ok"] is False
        assert "itself" in result["error"]

    def test_duplicate_detection(self):
        csd.declare_dependency("A", "B")
        result = csd.declare_dependency("A", "B")
        assert result["ok"] is True
        assert "already" in result.get("note", "")

    def test_persisted_to_disk(self):
        csd.declare_dependency("A", "B")
        raw = json.loads(csd._deps_path().read_text("utf-8"))
        assert len(raw["dependencies"]) == 1

    def test_unique_ids(self):
        r1 = csd.declare_dependency("A", "B")
        r2 = csd.declare_dependency("A", "C")
        assert r1["dependency"]["id"] != r2["dependency"]["id"]

    def test_met_defaults_false(self):
        result = csd.declare_dependency("A", "B")
        assert result["dependency"]["met"] is False


# ── get_dependencies ─────────────────────────────────────────────────────


class TestGetDependencies:
    def test_empty(self):
        assert csd.get_dependencies("nonexistent") == []

    def test_returns_own_deps(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("A", "C")
        csd.declare_dependency("D", "B")
        deps = csd.get_dependencies("A")
        assert len(deps) == 2
        targets = {d["depends_on"] for d in deps}
        assert targets == {"B", "C"}

    def test_ignores_other_swarms(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("X", "Y")
        assert len(csd.get_dependencies("A")) == 1


# ── get_dependents ───────────────────────────────────────────────────────


class TestGetDependents:
    def test_empty(self):
        assert csd.get_dependents("nonexistent") == []

    def test_returns_who_depends_on_me(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("C", "B")
        dependents = csd.get_dependents("B")
        assert len(dependents) == 2
        swarm_ids = {d["swarm_id"] for d in dependents}
        assert swarm_ids == {"A", "C"}

    def test_ignores_unrelated(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("X", "Y")
        assert len(csd.get_dependents("B")) == 1


# ── check_dependencies_met ──────────────────────────────────────────────


class TestCheckDependenciesMet:
    def test_no_deps_means_all_met(self):
        result = csd.check_dependencies_met("lonely")
        assert result["all_met"] is True
        assert result["blocking"] == []

    def test_unmet_dependency(self):
        csd.declare_dependency("A", "B")
        result = csd.check_dependencies_met("A")
        assert result["all_met"] is False
        assert "B" in result["blocking"]

    def test_after_completion(self):
        csd.declare_dependency("A", "B")
        csd.notify_completion("B")
        result = csd.check_dependencies_met("A")
        assert result["all_met"] is True

    def test_partial_completion(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("A", "C")
        csd.notify_completion("B")
        result = csd.check_dependencies_met("A")
        assert result["all_met"] is False
        assert result["blocking"] == ["C"]


# ── notify_completion ────────────────────────────────────────────────────


class TestNotifyCompletion:
    def test_marks_deps_met(self):
        csd.declare_dependency("A", "B")
        csd.notify_completion("B")
        deps = csd.get_dependencies("A")
        assert all(d["met"] for d in deps)

    def test_returns_unblocked_count(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("C", "B")
        result = csd.notify_completion("B")
        assert result["notified"] == 2
        assert set(result["swarms_unblocked"]) == {"A", "C"}

    def test_no_double_notification(self):
        csd.declare_dependency("A", "B")
        csd.notify_completion("B")
        result = csd.notify_completion("B")
        assert result["notified"] == 0

    def test_writes_notification_log(self):
        csd.declare_dependency("A", "B")
        csd.notify_completion("B")
        notes = csd.get_notifications()
        assert len(notes) == 1
        assert notes[0]["completed_swarm"] == "B"
        assert notes[0]["waiting_swarm"] == "A"


# ── get_dependency_graph ─────────────────────────────────────────────────


class TestGetDependencyGraph:
    def test_empty_graph(self):
        g = csd.get_dependency_graph()
        assert g["nodes"] == []
        assert g["edges"] == []

    def test_nodes_and_edges(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("C", "B")
        g = csd.get_dependency_graph()
        assert set(g["nodes"]) == {"A", "B", "C"}
        assert len(g["edges"]) == 2

    def test_edge_met_status(self):
        csd.declare_dependency("A", "B")
        csd.notify_completion("B")
        g = csd.get_dependency_graph()
        assert g["edges"][0]["met"] is True


# ── detect_circular ──────────────────────────────────────────────────────


class TestDetectCircular:
    def test_no_cycle(self):
        csd.declare_dependency("A", "B")
        assert csd.detect_circular("C", "A") is False

    def test_direct_cycle(self):
        csd.declare_dependency("A", "B")
        assert csd.detect_circular("B", "A") is True

    def test_self_cycle(self):
        assert csd.detect_circular("X", "X") is True

    def test_transitive_cycle(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("B", "C")
        assert csd.detect_circular("C", "A") is True

    def test_no_transitive_cycle(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("B", "C")
        assert csd.detect_circular("D", "C") is False

    def test_declaration_rejects_cycle(self):
        csd.declare_dependency("A", "B")
        result = csd.declare_dependency("B", "A")
        assert result["ok"] is False
        assert "cycle" in result["error"]


# ── remove_dependency ────────────────────────────────────────────────────


class TestRemoveDependency:
    def test_remove_existing(self):
        csd.declare_dependency("A", "B")
        result = csd.remove_dependency("A", "B")
        assert result["ok"] is True
        assert csd.get_dependencies("A") == []

    def test_remove_nonexistent(self):
        result = csd.remove_dependency("X", "Y")
        assert result["ok"] is False

    def test_remove_only_targeted(self):
        csd.declare_dependency("A", "B")
        csd.declare_dependency("A", "C")
        csd.remove_dependency("A", "B")
        deps = csd.get_dependencies("A")
        assert len(deps) == 1
        assert deps[0]["depends_on"] == "C"
