"""
Tests for infospace composition model (S2.6).
"""

from __future__ import annotations

from pathlib import Path

import pytest
import yaml

from markitect.infospace.composition import (
    DisciplineStatus,
    StaleMappingInfo,
    bind_discipline,
    check_discipline_status,
    compute_discipline_digests,
    find_stale_mappings,
    get_discipline_entities,
    load_discipline_config,
    resolve_discipline_path,
)
from markitect.infospace.config import (
    DisciplineBinding,
    InfospaceConfig,
    TopicConfig,
    ViabilityThreshold,
    save_infospace_config,
)


# ── helpers ──────────────────────────────────────────────────────────


def _create_discipline(tmp_path: Path, name: str = "test-discipline") -> Path:
    """Create a minimal discipline infospace directory."""
    disc_dir = tmp_path / name
    disc_dir.mkdir(parents=True, exist_ok=True)

    disc_config = InfospaceConfig(
        topic=TopicConfig(name=name.replace("-", " ").title(), domain="Testing"),
        viability={"coverage_ratio": ViabilityThreshold(metric="coverage_ratio", min=0.5)},
    )
    save_infospace_config(disc_config, disc_dir / "infospace.yaml")

    # Create some entities
    entities_dir = disc_dir / "output" / "entities"
    entities_dir.mkdir(parents=True, exist_ok=True)

    for slug in ["concept_a", "concept_b", "concept-c"]:
        title = slug.replace("-", " ").title()
        (entities_dir / f"{slug}.md").write_text(
            f"# {title}\n\n## Definition\n\nA test concept for {slug}.\n\n"
            f"## Source Chapter\n\nch01\n\n## Domain\n\nTesting\n",
            encoding="utf-8",
        )

    return disc_dir


def _parent_config(tmp_path: Path, disc_path: str = "") -> InfospaceConfig:
    """Create a parent infospace config."""
    return InfospaceConfig(
        topic=TopicConfig(name="Parent", domain="Testing"),
        disciplines=[DisciplineBinding(name="Test Discipline", path=disc_path)]
        if disc_path
        else [],
    )


# ── resolve_discipline_path ─────────────────────────────────────────


class TestResolveDisciplinePath:
    def test_relative_path(self, tmp_path):
        disc_dir = _create_discipline(tmp_path)
        binding = DisciplineBinding(name="test", path="test-discipline")
        result = resolve_discipline_path(binding, tmp_path)
        assert result is not None
        assert result == disc_dir.resolve()

    def test_absolute_path(self, tmp_path):
        disc_dir = _create_discipline(tmp_path)
        binding = DisciplineBinding(name="test", path=str(disc_dir))
        result = resolve_discipline_path(binding, tmp_path / "other")
        assert result is not None
        assert result == disc_dir.resolve()

    def test_missing_path(self, tmp_path):
        binding = DisciplineBinding(name="test", path="nonexistent")
        assert resolve_discipline_path(binding, tmp_path) is None

    def test_empty_path(self, tmp_path):
        binding = DisciplineBinding(name="test", path="")
        assert resolve_discipline_path(binding, tmp_path) is None


# ── load_discipline_config ──────────────────────────────────────────


class TestLoadDisciplineConfig:
    def test_loads_config(self, tmp_path):
        disc_dir = _create_discipline(tmp_path)
        binding = DisciplineBinding(name="test", path="test-discipline")
        config = load_discipline_config(binding, tmp_path)
        assert config is not None
        assert config.topic.domain == "Testing"

    def test_missing_config_file(self, tmp_path):
        (tmp_path / "no-config").mkdir()
        binding = DisciplineBinding(name="test", path="no-config")
        assert load_discipline_config(binding, tmp_path) is None

    def test_missing_directory(self, tmp_path):
        binding = DisciplineBinding(name="test", path="gone")
        assert load_discipline_config(binding, tmp_path) is None


# ── check_discipline_status ─────────────────────────────────────────


class TestCheckDisciplineStatus:
    def test_valid_discipline(self, tmp_path):
        _create_discipline(tmp_path)
        binding = DisciplineBinding(name="Test Discipline", path="test-discipline")
        status = check_discipline_status(binding, tmp_path)
        assert status.exists
        assert status.has_config
        assert status.entity_count == 3
        assert status.error == ""

    def test_missing_discipline(self, tmp_path):
        binding = DisciplineBinding(name="Missing", path="nope")
        status = check_discipline_status(binding, tmp_path)
        assert not status.exists
        assert "not found" in status.error.lower()

    def test_no_config(self, tmp_path):
        (tmp_path / "bare").mkdir()
        binding = DisciplineBinding(name="Bare", path="bare")
        status = check_discipline_status(binding, tmp_path)
        assert status.exists
        assert not status.has_config

    def test_viable_with_metrics(self, tmp_path):
        disc_dir = _create_discipline(tmp_path)
        # Write metrics that meet the threshold
        metrics_dir = disc_dir / "output" / "metrics"
        metrics_dir.mkdir(parents=True, exist_ok=True)
        (metrics_dir / "metrics.yaml").write_text(
            yaml.safe_dump({"coverage_ratio": 0.8}), encoding="utf-8"
        )
        binding = DisciplineBinding(name="Test", path="test-discipline")
        status = check_discipline_status(binding, tmp_path)
        assert status.is_viable

    def test_not_viable_below_threshold(self, tmp_path):
        disc_dir = _create_discipline(tmp_path)
        metrics_dir = disc_dir / "output" / "metrics"
        metrics_dir.mkdir(parents=True, exist_ok=True)
        (metrics_dir / "metrics.yaml").write_text(
            yaml.safe_dump({"coverage_ratio": 0.2}), encoding="utf-8"
        )
        binding = DisciplineBinding(name="Test", path="test-discipline")
        status = check_discipline_status(binding, tmp_path)
        assert not status.is_viable

    def test_to_dict(self, tmp_path):
        _create_discipline(tmp_path)
        binding = DisciplineBinding(name="Test", path="test-discipline")
        status = check_discipline_status(binding, tmp_path)
        d = status.to_dict()
        assert d["name"] == "Test"
        assert d["exists"] is True
        assert d["entity_count"] == 3


# ── get_discipline_entities ─────────────────────────────────────────


class TestGetDisciplineEntities:
    def test_returns_entities(self, tmp_path):
        _create_discipline(tmp_path)
        binding = DisciplineBinding(name="Test", path="test-discipline")
        entities = get_discipline_entities(binding, tmp_path)
        assert len(entities) == 3
        slugs = {e.slug for e in entities}
        assert "concept_a" in slugs

    def test_missing_discipline(self, tmp_path):
        binding = DisciplineBinding(name="Test", path="nope")
        assert get_discipline_entities(binding, tmp_path) == []


# ── compute_discipline_digests ──────────────────────────────────────


class TestComputeDisciplineDigests:
    def test_returns_digests(self, tmp_path):
        _create_discipline(tmp_path)
        binding = DisciplineBinding(name="Test", path="test-discipline")
        digests = compute_discipline_digests(binding, tmp_path)
        assert len(digests) == 3
        assert "concept_a" in digests
        assert isinstance(digests["concept_a"], str)
        assert len(digests["concept_a"]) == 12


# ── find_stale_mappings ─────────────────────────────────────────────


class TestFindStaleMappings:
    def test_no_references(self, tmp_path):
        cfg = _parent_config(tmp_path, disc_path="test-discipline")
        assert find_stale_mappings(cfg, tmp_path) == []

    def test_no_stale(self, tmp_path):
        _create_discipline(tmp_path)
        cfg = _parent_config(tmp_path, disc_path="test-discipline")
        refs = {"entity_x": ["concept_a", "concept_b"]}
        stale = find_stale_mappings(cfg, tmp_path, mapping_references=refs)
        assert stale == []

    def test_detects_stale(self, tmp_path):
        _create_discipline(tmp_path)
        cfg = _parent_config(tmp_path, disc_path="test-discipline")
        refs = {"entity_x": ["concept_a", "deleted_concept"]}
        stale = find_stale_mappings(cfg, tmp_path, mapping_references=refs)
        assert len(stale) == 1
        assert stale[0].entity_slug == "entity_x"
        assert stale[0].discipline_entity == "deleted_concept"

    def test_stale_to_dict(self):
        info = StaleMappingInfo(
            entity_slug="e1", discipline_entity="d1", reason="gone"
        )
        d = info.to_dict()
        assert d["entity_slug"] == "e1"


# ── bind_discipline ─────────────────────────────────────────────────


class TestBindDiscipline:
    def test_adds_binding(self, tmp_path):
        _create_discipline(tmp_path)
        cfg = InfospaceConfig(topic=TopicConfig(name="Parent"))
        status = bind_discipline(cfg, name="Test", path="test-discipline", root=tmp_path)
        assert status.exists
        assert len(cfg.disciplines) == 1
        assert cfg.disciplines[0].name == "Test"

    def test_duplicate_rejected(self, tmp_path):
        _create_discipline(tmp_path)
        cfg = _parent_config(tmp_path, disc_path="test-discipline")
        status = bind_discipline(cfg, name="Test Discipline", path="x", root=tmp_path)
        assert "already bound" in status.error
