"""Tests for markitect.infospace.config and state."""

from datetime import datetime
from pathlib import Path

import pytest

from markitect.infospace.config import (
    DisciplineBinding,
    InfospaceConfig,
    PipelineConfig,
    PipelineStage,
    SchemaRegistry,
    TopicConfig,
    ViabilityThreshold,
    find_infospace_config,
    load_infospace_config,
    save_infospace_config,
)
from markitect.infospace.state import (
    InfospaceState,
    ViabilityResult,
    build_state,
)
from markitect.infospace.models import EntityMeta
from markitect.infospace.evaluation import (
    EntityEvaluation,
    EvaluationSnapshot,
    ScoreEntry,
)


# ── Helpers ──────────────────────────────────────────────────────────


_SAMPLE_YAML = """\
topic:
  name: "The Wealth of Nations"
  domain: "Classical Economics"
  sources: artifacts/sources/

disciplines:
  - name: "Viable System Model"
    path: artifacts/vsm-reference/

schemas:
  entity: schemas/economic-entity-schema-v1.0.md
  mapping: schemas/vsm-mapping-schema-v1.0.md

competency_questions: schemas/competency-questions.md

viability:
  coverage_ratio:
    min: 0.60
  per_entity_mean:
    min: 3.5
  redundancy_ratio:
    max: 0.05

pipeline:
  stages:
    - template: extract-entities
      spaces: [sources, guidelines]
    - template: map-to-vsm
      spaces: [entities, vsm-reference]
  post_batch:
    - template: assess-metrics
"""


def _sample_config() -> InfospaceConfig:
    return InfospaceConfig(
        topic=TopicConfig(name="Test Topic", domain="Testing"),
        disciplines=[DisciplineBinding(name="VSM", path="vsm/")],
        schemas=SchemaRegistry(entity="schemas/entity.md"),
        competency_questions="schemas/cq.md",
        viability={
            "coverage_ratio": ViabilityThreshold("coverage_ratio", min=0.6),
            "redundancy_ratio": ViabilityThreshold("redundancy_ratio", max=0.05),
        },
    )


def _sample_entities(n=5) -> list:
    return [
        EntityMeta(
            slug=f"entity-{i}",
            title=f"Entity {i}",
            h1_raw=f"Entity {i}",
            domain="Production" if i % 2 == 0 else "Distribution",
        )
        for i in range(n)
    ]


# ── TopicConfig ──────────────────────────────────────────────────────


class TestTopicConfig:
    def test_round_trip(self):
        tc = TopicConfig("WoN", "Economics", "sources/")
        d = tc.to_dict()
        restored = TopicConfig.from_dict(d)
        assert restored.name == "WoN"
        assert restored.domain == "Economics"
        assert restored.sources == "sources/"

    def test_minimal(self):
        tc = TopicConfig.from_dict({"name": "Minimal"})
        assert tc.domain == ""
        assert tc.sources == ""

    def test_to_dict_omits_empty(self):
        tc = TopicConfig("X")
        d = tc.to_dict()
        assert "domain" not in d
        assert "sources" not in d


# ── DisciplineBinding ────────────────────────────────────────────────


class TestDisciplineBinding:
    def test_round_trip(self):
        db = DisciplineBinding("VSM", "path/to/vsm")
        d = db.to_dict()
        restored = DisciplineBinding.from_dict(d)
        assert restored.name == "VSM"
        assert restored.path == "path/to/vsm"


# ── SchemaRegistry ───────────────────────────────────────────────────


class TestSchemaRegistry:
    def test_round_trip(self):
        sr = SchemaRegistry(entity="e.md", mapping="m.md", analysis="a.md")
        d = sr.to_dict()
        restored = SchemaRegistry.from_dict(d)
        assert restored.entity == "e.md"
        assert restored.mapping == "m.md"

    def test_extra_schemas(self):
        sr = SchemaRegistry.from_dict({"entity": "e.md", "custom": "c.md"})
        assert sr.entity == "e.md"
        assert sr.extra == {"custom": "c.md"}


# ── ViabilityThreshold ──────────────────────────────────────────────


class TestViabilityThreshold:
    def test_min_check(self):
        t = ViabilityThreshold("x", min=0.5)
        assert t.check(0.6) is True
        assert t.check(0.5) is True
        assert t.check(0.4) is False

    def test_max_check(self):
        t = ViabilityThreshold("x", max=0.1)
        assert t.check(0.05) is True
        assert t.check(0.1) is True
        assert t.check(0.2) is False

    def test_min_and_max(self):
        t = ViabilityThreshold("x", min=0.3, max=0.7)
        assert t.check(0.5) is True
        assert t.check(0.2) is False
        assert t.check(0.8) is False

    def test_no_bounds_always_passes(self):
        t = ViabilityThreshold("x")
        assert t.check(999.0) is True


# ── PipelineConfig ──────────────────────────────────────────────────


class TestPipelineConfig:
    def test_round_trip(self):
        pc = PipelineConfig(
            stages=[PipelineStage("extract", ["s1", "s2"])],
            post_batch=[PipelineStage("assess")],
        )
        d = pc.to_dict()
        restored = PipelineConfig.from_dict(d)
        assert len(restored.stages) == 1
        assert restored.stages[0].template == "extract"
        assert restored.stages[0].spaces == ["s1", "s2"]
        assert len(restored.post_batch) == 1


# ── InfospaceConfig ─────────────────────────────────────────────────


class TestInfospaceConfig:
    def test_to_dict_from_dict_round_trip(self):
        cfg = _sample_config()
        d = cfg.to_dict()
        restored = InfospaceConfig.from_dict(d)
        assert restored.topic.name == "Test Topic"
        assert len(restored.disciplines) == 1
        assert restored.schemas.entity == "schemas/entity.md"
        assert restored.competency_questions == "schemas/cq.md"
        assert len(restored.viability) == 2

    def test_viability_thresholds_preserved(self):
        cfg = _sample_config()
        d = cfg.to_dict()
        restored = InfospaceConfig.from_dict(d)
        assert restored.viability["coverage_ratio"].min == 0.6
        assert restored.viability["redundancy_ratio"].max == 0.05

    def test_default_dirs(self):
        cfg = InfospaceConfig(topic=TopicConfig("X"))
        assert cfg.entities_dir == "output/entities"
        assert cfg.evaluations_dir == "output/evaluations"
        assert cfg.metrics_dir == "output/metrics"

    def test_custom_dirs(self):
        cfg = InfospaceConfig.from_dict({
            "topic": {"name": "X"},
            "entities_dir": "custom/entities",
        })
        assert cfg.entities_dir == "custom/entities"


# ── YAML I/O ────────────────────────────────────────────────────────


class TestYAMLIO:
    def test_save_load_round_trip(self, tmp_path):
        cfg = _sample_config()
        p = tmp_path / "infospace.yaml"
        save_infospace_config(cfg, p)
        loaded = load_infospace_config(p)
        assert loaded.topic.name == cfg.topic.name
        assert len(loaded.viability) == len(cfg.viability)

    def test_load_full_example(self, tmp_path):
        p = tmp_path / "infospace.yaml"
        p.write_text(_SAMPLE_YAML, encoding="utf-8")
        cfg = load_infospace_config(p)
        assert cfg.topic.name == "The Wealth of Nations"
        assert cfg.topic.domain == "Classical Economics"
        assert len(cfg.disciplines) == 1
        assert cfg.disciplines[0].name == "Viable System Model"
        assert cfg.schemas.entity == "schemas/economic-entity-schema-v1.0.md"
        assert cfg.competency_questions == "schemas/competency-questions.md"
        assert len(cfg.viability) == 3
        assert cfg.viability["coverage_ratio"].min == 0.60
        assert cfg.viability["redundancy_ratio"].max == 0.05
        assert cfg.pipeline is not None
        assert len(cfg.pipeline.stages) == 2
        assert len(cfg.pipeline.post_batch) == 1

    def test_load_missing_file(self, tmp_path):
        with pytest.raises(FileNotFoundError):
            load_infospace_config(tmp_path / "nonexistent.yaml")

    def test_load_missing_topic(self, tmp_path):
        p = tmp_path / "bad.yaml"
        p.write_text("schemas:\n  entity: x.md\n")
        with pytest.raises(ValueError, match="topic"):
            load_infospace_config(p)

    def test_save_creates_parent_dirs(self, tmp_path):
        cfg = InfospaceConfig(topic=TopicConfig("X"))
        p = tmp_path / "deep" / "nested" / "infospace.yaml"
        save_infospace_config(cfg, p)
        assert p.exists()


class TestFindConfig:
    def test_finds_config_in_current_dir(self, tmp_path):
        (tmp_path / "infospace.yaml").write_text("topic:\n  name: X\n")
        found = find_infospace_config(tmp_path)
        assert found is not None
        assert found.name == "infospace.yaml"

    def test_finds_config_in_parent(self, tmp_path):
        (tmp_path / "infospace.yaml").write_text("topic:\n  name: X\n")
        child = tmp_path / "sub" / "dir"
        child.mkdir(parents=True)
        found = find_infospace_config(child)
        assert found is not None

    def test_returns_none_if_not_found(self, tmp_path):
        assert find_infospace_config(tmp_path) is None


# ── InfospaceState ──────────────────────────────────────────────────


class TestInfospaceState:
    def test_entity_count(self):
        cfg = _sample_config()
        state = InfospaceState(config=cfg, entities=_sample_entities(5))
        assert state.entity_count == 5

    def test_topic_name(self):
        cfg = _sample_config()
        state = InfospaceState(config=cfg)
        assert state.topic_name == "Test Topic"

    def test_domains(self):
        cfg = _sample_config()
        state = InfospaceState(config=cfg, entities=_sample_entities(4))
        assert "Production" in state.domains
        assert "Distribution" in state.domains

    def test_has_evaluations(self):
        cfg = _sample_config()
        state = InfospaceState(config=cfg)
        assert state.has_evaluations is False

        snap = EvaluationSnapshot(
            snapshot_id="s1",
            created_at=datetime(2026, 1, 1),
            schema_name="Test",
            entity_count=0,
        )
        state.latest_snapshot = snap
        assert state.has_evaluations is True


class TestViabilityCheck:
    def test_all_pass(self):
        cfg = _sample_config()
        state = InfospaceState(config=cfg)
        metrics = {"coverage_ratio": 0.8, "redundancy_ratio": 0.02}
        results = state.check_viability(metrics)
        assert all(r.passed for r in results)
        assert state.is_viable is True

    def test_one_fails(self):
        cfg = _sample_config()
        state = InfospaceState(config=cfg)
        metrics = {"coverage_ratio": 0.4, "redundancy_ratio": 0.02}
        results = state.check_viability(metrics)
        assert not all(r.passed for r in results)
        assert state.is_viable is False

    def test_missing_metric_defaults_to_zero(self):
        cfg = _sample_config()
        state = InfospaceState(config=cfg)
        # coverage_ratio min=0.6, missing → 0.0 → fails
        results = state.check_viability({})
        coverage = next(r for r in results if r.metric == "coverage_ratio")
        assert coverage.passed is False
        assert coverage.value == 0.0

    def test_viability_counts(self):
        cfg = _sample_config()
        state = InfospaceState(config=cfg)
        metrics = {"coverage_ratio": 0.8, "redundancy_ratio": 0.2}
        state.check_viability(metrics)
        assert state.viability_pass_count == 1  # coverage passes
        assert state.viability_total_count == 2

    def test_no_thresholds_not_viable(self):
        cfg = InfospaceConfig(topic=TopicConfig("X"))
        state = InfospaceState(config=cfg)
        assert state.is_viable is False


class TestBuildState:
    def test_builds_with_entities(self):
        cfg = _sample_config()
        entities = _sample_entities(3)
        state = build_state(cfg, entities=entities)
        assert state.entity_count == 3

    def test_builds_with_metrics(self):
        cfg = _sample_config()
        metrics = {"coverage_ratio": 0.9, "redundancy_ratio": 0.01}
        state = build_state(cfg, metrics=metrics)
        assert state.is_viable is True

    def test_summary(self):
        cfg = _sample_config()
        entities = _sample_entities(3)
        metrics = {"coverage_ratio": 0.9, "redundancy_ratio": 0.01}
        state = build_state(cfg, entities=entities, metrics=metrics)
        s = state.summary()
        assert s["topic"] == "Test Topic"
        assert s["entity_count"] == 3
        assert s["viable"] is True


class TestViabilityResult:
    def test_to_dict(self):
        t = ViabilityThreshold("x", min=0.5)
        r = ViabilityResult(metric="x", value=0.7, threshold=t, passed=True)
        d = r.to_dict()
        assert d["metric"] == "x"
        assert d["value"] == 0.7
        assert d["passed"] is True
        assert d["min"] == 0.5
        assert "max" not in d
