"""Tests for markitect.infospace.cli."""

from pathlib import Path

import pytest
from click.testing import CliRunner

from markitect.infospace.cli import infospace_commands


@pytest.fixture
def runner():
    return CliRunner()


@pytest.fixture
def infospace_dir(tmp_path):
    """Create a minimal infospace directory with config and entities."""
    config_yaml = """\
topic:
  name: "Test Infospace"
  domain: "Testing"

disciplines:
  - name: "Test Discipline"

viability:
  coverage_ratio:
    min: 0.60
  redundancy_ratio:
    max: 0.05
"""
    (tmp_path / "infospace.yaml").write_text(config_yaml)

    entities = tmp_path / "output" / "entities"
    entities.mkdir(parents=True)
    (entities / "alpha.md").write_text(
        "# Alpha\n\n## Definition\n\nAlpha is a test entity.\n\n"
        "## Source Chapter\n\nChapter 1\n\n"
        "## Domain\n\nProduction\n"
    )
    (entities / "beta.md").write_text(
        "# Beta\n\n## Definition\n\nBeta is another test entity with more words "
        "to make it longer.\n\n"
        "## Source Chapter\n\nChapter 2\n\n"
        "## Domain\n\nDistribution\n"
    )
    return tmp_path


# ── init ─────────────────────────────────────────────────────────────


class TestInitCommand:
    def test_creates_config_file(self, runner, tmp_path):
        out = tmp_path / "infospace.yaml"
        result = runner.invoke(
            infospace_commands,
            ["init", "--topic", "My Topic", "--domain", "Science", "-o", str(out)],
        )
        assert result.exit_code == 0
        assert out.exists()
        assert "Created" in result.output

    def test_config_contains_topic(self, runner, tmp_path):
        out = tmp_path / "infospace.yaml"
        runner.invoke(
            infospace_commands,
            ["init", "--topic", "My Topic", "-o", str(out)],
        )
        text = out.read_text()
        assert "My Topic" in text

    def test_refuses_overwrite(self, runner, tmp_path):
        out = tmp_path / "infospace.yaml"
        out.write_text("existing")
        result = runner.invoke(
            infospace_commands,
            ["init", "--topic", "X", "-o", str(out)],
        )
        assert result.exit_code != 0
        assert "already exists" in result.output

    def test_with_disciplines(self, runner, tmp_path):
        out = tmp_path / "infospace.yaml"
        result = runner.invoke(
            infospace_commands,
            [
                "init", "--topic", "T",
                "--discipline", "VSM",
                "--discipline", "Category Theory",
                "-o", str(out),
            ],
        )
        assert result.exit_code == 0
        text = out.read_text()
        assert "VSM" in text
        assert "Category Theory" in text


# ── status ───────────────────────────────────────────────────────────


class TestStatusCommand:
    def test_shows_topic_and_count(self, runner, infospace_dir):
        result = runner.invoke(
            infospace_commands,
            ["status", "--config", str(infospace_dir / "infospace.yaml")],
        )
        assert result.exit_code == 0
        assert "Test Infospace" in result.output
        assert "2" in result.output  # 2 entities

    def test_shows_domain_field(self, runner, infospace_dir):
        result = runner.invoke(
            infospace_commands,
            ["status", "--config", str(infospace_dir / "infospace.yaml")],
        )
        # Domain from config (topic.domain), not entity domains
        assert "Testing" in result.output

    def test_shows_disciplines(self, runner, infospace_dir):
        result = runner.invoke(
            infospace_commands,
            ["status", "--config", str(infospace_dir / "infospace.yaml")],
        )
        assert "Test Discipline" in result.output

    def test_no_config_exits(self, runner, tmp_path):
        result = runner.invoke(
            infospace_commands,
            ["status", "--config", str(tmp_path / "nonexistent.yaml")],
        )
        assert result.exit_code != 0


# ── entities ─────────────────────────────────────────────────────────


class TestEntitiesCommand:
    def test_lists_entities(self, runner, infospace_dir):
        result = runner.invoke(
            infospace_commands,
            ["entities", "--config", str(infospace_dir / "infospace.yaml")],
        )
        assert result.exit_code == 0
        assert "alpha" in result.output
        assert "beta" in result.output
        assert "Total: 2" in result.output

    def test_sort_by_domain(self, runner, infospace_dir):
        result = runner.invoke(
            infospace_commands,
            [
                "entities",
                "--config", str(infospace_dir / "infospace.yaml"),
                "--sort-by", "domain",
            ],
        )
        assert result.exit_code == 0
        lines = result.output.strip().split("\n")
        # Distribution comes before Production alphabetically
        data_lines = [l for l in lines if "alpha" in l or "beta" in l]
        assert len(data_lines) == 2

    def test_no_entities_dir(self, runner, tmp_path):
        (tmp_path / "infospace.yaml").write_text("topic:\n  name: X\n")
        result = runner.invoke(
            infospace_commands,
            ["entities", "--config", str(tmp_path / "infospace.yaml")],
        )
        assert result.exit_code == 0
        assert "No entities" in result.output


# ── viability ────────────────────────────────────────────────────────


class TestViabilityCommand:
    def test_no_metrics_shows_thresholds(self, runner, infospace_dir):
        result = runner.invoke(
            infospace_commands,
            ["viability", "--config", str(infospace_dir / "infospace.yaml")],
        )
        assert result.exit_code == 0
        assert "coverage_ratio" in result.output

    def test_with_metrics_file(self, runner, infospace_dir):
        import yaml
        metrics_dir = infospace_dir / "output" / "metrics"
        metrics_dir.mkdir(parents=True, exist_ok=True)
        metrics = {"coverage_ratio": 0.85, "redundancy_ratio": 0.02}
        (metrics_dir / "metrics.yaml").write_text(yaml.safe_dump(metrics))

        result = runner.invoke(
            infospace_commands,
            ["viability", "--config", str(infospace_dir / "infospace.yaml")],
        )
        assert result.exit_code == 0
        assert "PASS" in result.output
        assert "Viable: YES" in result.output

    def test_failing_threshold(self, runner, infospace_dir):
        import yaml
        metrics_dir = infospace_dir / "output" / "metrics"
        metrics_dir.mkdir(parents=True, exist_ok=True)
        metrics = {"coverage_ratio": 0.3, "redundancy_ratio": 0.02}
        (metrics_dir / "metrics.yaml").write_text(yaml.safe_dump(metrics))

        result = runner.invoke(
            infospace_commands,
            ["viability", "--config", str(infospace_dir / "infospace.yaml")],
        )
        assert result.exit_code == 0
        assert "FAIL" in result.output
        assert "Viable: NO" in result.output

    def test_no_thresholds_configured(self, runner, tmp_path):
        (tmp_path / "infospace.yaml").write_text("topic:\n  name: X\n")
        result = runner.invoke(
            infospace_commands,
            ["viability", "--config", str(tmp_path / "infospace.yaml")],
        )
        assert result.exit_code == 0
        assert "No viability thresholds" in result.output


# ── chapters (per-source triage view) ────────────────────────────────


class TestChaptersCommand:
    @pytest.fixture
    def chapters_dir(self, tmp_path):
        """Infospace with 2 source files and matching entities."""
        config_yaml = """\
topic:
  name: "WoN"
  domain: "Economics"
  sources: artifacts/sources
"""
        (tmp_path / "infospace.yaml").write_text(config_yaml)

        sources = tmp_path / "artifacts" / "sources"
        sources.mkdir(parents=True)
        (sources / "book-1-chapter-01.md").write_text("# Chapter 1\n\nText.\n")
        (sources / "book-1-chapter-02.md").write_text("# Chapter 2\n\nText.\n")

        entities = tmp_path / "output" / "entities"
        entities.mkdir(parents=True)
        (entities / "alpha.md").write_text(
            "# Alpha\n\n## Definition\n\nX.\n\n"
            "## Source Chapter\n\nBook I, Chapter 1\n"
        )
        (entities / "beta.md").write_text(
            "# Beta\n\n## Definition\n\nY.\n\n"
            "## Source Chapter\n\nBook I, Chapter 2\n"
        )
        (entities / "gamma.md").write_text(
            "# Gamma\n\n## Definition\n\nZ.\n\n"
            "## Source Chapter\n\nBook I, Chapter 2\n"
        )
        return tmp_path

    def test_lists_sources_with_counts(self, runner, chapters_dir):
        result = runner.invoke(
            infospace_commands,
            ["chapters", "--config", str(chapters_dir / "infospace.yaml")],
        )
        assert result.exit_code == 0
        assert "book-1-chapter-01" in result.output
        assert "book-1-chapter-02" in result.output
        # ch 1 -> 1 entity, ch 2 -> 2 entities
        assert "2 source file(s); 3 entities" in result.output

    def test_json_format(self, runner, chapters_dir):
        result = runner.invoke(
            infospace_commands,
            ["chapters", "--config", str(chapters_dir / "infospace.yaml"),
             "--format", "json"],
        )
        assert result.exit_code == 0
        import json
        rows = json.loads(result.output)
        by_id = {r["source_id"]: r for r in rows}
        assert by_id["book-1-chapter-01"]["entities"] == 1
        assert by_id["book-1-chapter-02"]["entities"] == 2

    def test_no_sources_dir(self, runner, tmp_path):
        (tmp_path / "infospace.yaml").write_text(
            "topic:\n  name: X\n  sources: missing\n"
        )
        result = runner.invoke(
            infospace_commands,
            ["chapters", "--config", str(tmp_path / "infospace.yaml")],
        )
        assert result.exit_code == 1


# ── process: eval-after-source / classify-after-source flags ─────────


class TestProcessAfterSourceFlags:
    def test_flags_registered_in_help(self, runner):
        result = runner.invoke(infospace_commands, ["process", "--help"])
        assert result.exit_code == 0
        assert "--eval-after-source" in result.output
        assert "--classify-after-source" in result.output

    def test_flags_require_provider(self, runner, tmp_path):
        (tmp_path / "infospace.yaml").write_text(
            "topic:\n  name: X\n  sources: sources\n"
            "pipeline:\n  stages:\n    - template: extract-entities\n"
        )
        sources = tmp_path / "sources"
        sources.mkdir()
        (sources / "s1.md").write_text("source")
        result = runner.invoke(
            infospace_commands,
            ["process", "--all",
             "--config", str(tmp_path / "infospace.yaml"),
             "--eval-after-source"],
        )
        assert result.exit_code == 1
        assert "require --provider" in result.output


# ── pipeline: commit body composition ────────────────────────────────


class TestCommitBodyComposition:
    def test_bucket_for(self, tmp_path):
        from markitect.infospace.config import InfospaceConfig, TopicConfig
        from markitect.infospace.pipeline import SourcePipeline
        cfg = InfospaceConfig(topic=TopicConfig(name="T", domain="D"))
        p = SourcePipeline(cfg, tmp_path)
        assert p._bucket_for("output/entities/x.md") == "entities"
        assert p._bucket_for("output/evaluations/x.md") == "evaluations"
        assert p._bucket_for("output/classifications/x.md") == "classifications"
        assert p._bucket_for("output/mappings/x.md") == "mappings"
        assert p._bucket_for("output/notes/x.md") == "other"
        assert p._bucket_for("README.md") is None  # not under output/

    def test_compose_body_uses_default_on_no_diff(self, tmp_path):
        """When git diff fails or returns empty, fall back to the default blurb."""
        from markitect.infospace.config import InfospaceConfig, TopicConfig
        from markitect.infospace.pipeline import SourcePipeline
        cfg = InfospaceConfig(topic=TopicConfig(name="T", domain="D"))
        # Not a git repo, so `git diff --cached` will raise CalledProcessError.
        p = SourcePipeline(cfg, tmp_path)
        body = p._compose_commit_body("some-source")
        assert "Extract entities" in body
