"""Integration tests for BenchmarkManager LOC tracking.

Tests end-to-end workflows for the daily LOC tracker: build run lifecycle,
multi-approach comparisons, defect density calculations, and edge cases.
"""
import pytest
from greymatter_core.db import SQLiteDatabase
from greymatter_core.benchmarks import BenchmarkManager, VALID_APPROACHES, VALID_SEVERITIES


@pytest.fixture
def db():
    database = SQLiteDatabase(":memory:")
    yield database
    database.close()


@pytest.fixture
def benchmarks(db):
    return BenchmarkManager(db)


def _seed_standard(db, standard_id: str) -> None:
    """Seed a quality standard for scoring tests."""
    db.execute(
        """INSERT INTO quality_standards (id, category, rule, severity)
           VALUES (?, 'test', 'Test rule', 'critical')""",
        (standard_id,),
    )


# ---------------------------------------------------------------------------
# Happy path: full lifecycle
# ---------------------------------------------------------------------------


class TestFullLifecycle:
    """End-to-end lifecycle: start → complete → score → issue → resolve → report."""

    def test_full_build_run_lifecycle(self, benchmarks, db):
        """A single run through the entire tracking pipeline."""
        _seed_standard(db, "qs-typing")
        _seed_standard(db, "qs-sql")

        # 1. Start a run
        run = benchmarks.start_run(approach="greymatter", task_name="Build auth module")
        run_id = run["id"]
        assert run_id.startswith("br-")
        assert run["completed_at"] is None
        assert run["lines_written"] is None

        # 2. Complete with LOC metrics
        completed = benchmarks.complete_run(run_id, lines=350, tests=12, passing=12, first_pass=True)
        assert completed["lines_written"] == 350
        assert completed["test_count"] == 12
        assert completed["tests_passing"] == 12
        assert completed["first_pass_success"] == 1
        assert completed["completed_at"] is not None

        # 3. Score against quality standards
        benchmarks.score_run(run_id, "qs-typing", passed=True, found_by="review")
        benchmarks.score_run(run_id, "qs-sql", passed=True, found_by="review")
        scores = benchmarks.get_scores(run_id)
        assert len(scores) == 2
        assert all(s["passed"] == 1 for s in scores)

        # 4. Record and resolve an issue
        issue_id = benchmarks.record_issue(
            run_id, severity="minor", category="naming",
            description="Variable name too short", found_by="code_review",
        )
        assert issue_id > 0
        benchmarks.resolve_issue(issue_id, fix_minutes=3)
        issues = benchmarks.get_issues(run_id)
        assert len(issues) == 1
        assert issues[0]["fixed_at"] is not None
        assert issues[0]["time_to_fix_minutes"] == 3

        # 5. Summary reflects everything
        summary = benchmarks.summary()
        assert len(summary) == 1
        assert summary[0]["approach"] == "greymatter"
        assert summary[0]["total_lines"] == 350
        assert summary[0]["total_tests"] == 12
        assert summary[0]["total_issues"] == 1

    def test_multiple_runs_same_approach(self, benchmarks):
        """Multiple runs accumulate correctly in summary."""
        for i, lines in enumerate([100, 250, 175]):
            run = benchmarks.start_run(approach="subagent", task_name=f"Task {i}")
            benchmarks.complete_run(run["id"], lines=lines, tests=5, passing=5, first_pass=True)

        summary = benchmarks.summary()
        assert len(summary) == 1
        row = summary[0]
        assert row["total_lines"] == 525
        assert row["total_runs"] == 3
        assert row["total_tests"] == 15


# ---------------------------------------------------------------------------
# Multi-approach comparison (the core LOC tracker use case)
# ---------------------------------------------------------------------------


class TestMultiApproachComparison:
    """Compare LOC production and quality across approaches."""

    def _seed_approach(self, benchmarks, approach: str, runs: list[dict]) -> None:
        for r in runs:
            run = benchmarks.start_run(approach=approach, task_name=r["task"])
            benchmarks.complete_run(
                run["id"], lines=r["lines"], tests=r["tests"],
                passing=r["passing"], first_pass=r["first_pass"],
            )
            for _ in range(r.get("issues", 0)):
                benchmarks.record_issue(
                    run["id"], severity="minor", category="style",
                    description="Issue", found_by="review",
                )

    def test_approach_comparison_summary(self, benchmarks):
        """Summary correctly groups and aggregates by approach."""
        self._seed_approach(benchmarks, "greymatter", [
            {"task": "Auth", "lines": 300, "tests": 10, "passing": 10, "first_pass": True, "issues": 1},
            {"task": "DB", "lines": 200, "tests": 8, "passing": 8, "first_pass": True, "issues": 0},
        ])
        self._seed_approach(benchmarks, "subagent", [
            {"task": "Auth", "lines": 280, "tests": 6, "passing": 6, "first_pass": False, "issues": 3},
        ])

        summary = benchmarks.summary()
        approaches = {r["approach"]: r for r in summary}
        assert "greymatter" in approaches
        assert "subagent" in approaches

        gm = approaches["greymatter"]
        assert gm["total_lines"] == 500
        assert gm["total_runs"] == 2
        assert gm["total_issues"] == 1

        sub = approaches["subagent"]
        assert sub["total_lines"] == 280
        assert sub["total_runs"] == 1
        assert sub["total_issues"] == 3

    def test_defect_density_comparison(self, benchmarks):
        """Defect density correctly calculated across approaches."""
        self._seed_approach(benchmarks, "greymatter", [
            {"task": "T1", "lines": 1000, "tests": 20, "passing": 20, "first_pass": True, "issues": 2},
        ])
        self._seed_approach(benchmarks, "raw_claude", [
            {"task": "T1", "lines": 1000, "tests": 15, "passing": 15, "first_pass": False, "issues": 10},
        ])

        density = benchmarks.defect_density()
        by_approach = {d["approach"]: d["defects_per_kloc"] for d in density}

        assert by_approach["greymatter"] == pytest.approx(2.0)
        assert by_approach["raw_claude"] == pytest.approx(10.0)

    def test_defect_density_filtered_by_approach(self, benchmarks):
        """Filtering defect density by approach returns only that approach."""
        self._seed_approach(benchmarks, "greymatter", [
            {"task": "T1", "lines": 500, "tests": 5, "passing": 5, "first_pass": True, "issues": 1},
        ])
        self._seed_approach(benchmarks, "plugin", [
            {"task": "T1", "lines": 500, "tests": 5, "passing": 5, "first_pass": True, "issues": 5},
        ])

        density = benchmarks.defect_density(approach="greymatter")
        assert len(density) == 1
        assert density[0]["approach"] == "greymatter"
        assert density[0]["defects_per_kloc"] == pytest.approx(2.0)

    def test_first_pass_quality_comparison(self, benchmarks, db):
        """First-pass quality calculated correctly per approach."""
        _seed_standard(db, "qs-s1")
        _seed_standard(db, "qs-s2")

        run_gm = benchmarks.start_run(approach="greymatter", task_name="T1")
        benchmarks.complete_run(run_gm["id"], lines=100, tests=5, passing=5, first_pass=True)
        benchmarks.score_run(run_gm["id"], "qs-s1", passed=True, found_by="review")
        benchmarks.score_run(run_gm["id"], "qs-s2", passed=True, found_by="review")

        run_sub = benchmarks.start_run(approach="subagent", task_name="T1")
        benchmarks.complete_run(run_sub["id"], lines=100, tests=5, passing=5, first_pass=False)
        benchmarks.score_run(run_sub["id"], "qs-s1", passed=True, found_by="review")
        benchmarks.score_run(run_sub["id"], "qs-s2", passed=False, found_by="review")

        quality = benchmarks.first_pass_quality()
        by_approach = {q["approach"]: q["first_pass_pct"] for q in quality}
        assert by_approach["greymatter"] == pytest.approx(100.0)
        assert by_approach["subagent"] == pytest.approx(50.0)

    def test_time_to_correct_comparison(self, benchmarks):
        """Average fix time aggregated by approach."""
        run_gm = benchmarks.start_run(approach="greymatter", task_name="T1")
        benchmarks.complete_run(run_gm["id"], lines=200, tests=5, passing=5, first_pass=True)
        issue1 = benchmarks.record_issue(run_gm["id"], "minor", "naming", "Bad", "review")
        issue2 = benchmarks.record_issue(run_gm["id"], "minor", "naming", "Bad2", "review")
        benchmarks.resolve_issue(issue1, fix_minutes=4)
        benchmarks.resolve_issue(issue2, fix_minutes=6)

        run_sub = benchmarks.start_run(approach="subagent", task_name="T1")
        benchmarks.complete_run(run_sub["id"], lines=200, tests=5, passing=5, first_pass=False)
        issue3 = benchmarks.record_issue(run_sub["id"], "critical", "typing", "Wrong", "review")
        benchmarks.resolve_issue(issue3, fix_minutes=30)

        ttc = benchmarks.time_to_correct()
        by_approach = {t["approach"]: t["avg_fix_minutes"] for t in ttc}
        assert by_approach["greymatter"] == pytest.approx(5.0)
        assert by_approach["subagent"] == pytest.approx(30.0)


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    """Edge cases and boundary conditions."""

    def test_zero_lines_written(self, benchmarks):
        """A run with zero lines written is valid (e.g., config-only change)."""
        run = benchmarks.start_run(approach="greymatter", task_name="Config update")
        completed = benchmarks.complete_run(run["id"], lines=0, tests=1, passing=1, first_pass=True)
        assert completed["lines_written"] == 0

        summary = benchmarks.summary()
        assert summary[0]["total_lines"] == 0

    def test_defect_density_zero_lines(self, benchmarks):
        """Defect density with zero lines returns None (no division by zero)."""
        run = benchmarks.start_run(approach="greymatter", task_name="Config")
        benchmarks.complete_run(run["id"], lines=0, tests=1, passing=1, first_pass=True)
        benchmarks.record_issue(run["id"], "minor", "style", "Issue", "review")

        density = benchmarks.defect_density()
        assert len(density) == 1
        assert density[0]["defects_per_kloc"] is None

    def test_summary_excludes_incomplete_runs(self, benchmarks):
        """Summary only includes completed runs."""
        run1 = benchmarks.start_run(approach="greymatter", task_name="Done")
        benchmarks.complete_run(run1["id"], lines=100, tests=5, passing=5, first_pass=True)

        # This run is started but never completed
        benchmarks.start_run(approach="greymatter", task_name="In Progress")

        summary = benchmarks.summary()
        assert len(summary) == 1
        assert summary[0]["total_runs"] == 1
        assert summary[0]["total_lines"] == 100

    def test_no_completed_runs_summary(self, benchmarks):
        """Summary with no completed runs returns empty list."""
        benchmarks.start_run(approach="greymatter", task_name="Incomplete")
        summary = benchmarks.summary()
        assert summary == []

    def test_no_issues_defect_density(self, benchmarks):
        """Defect density with no issues returns zero."""
        run = benchmarks.start_run(approach="greymatter", task_name="Clean")
        benchmarks.complete_run(run["id"], lines=500, tests=20, passing=20, first_pass=True)

        density = benchmarks.defect_density()
        assert len(density) == 1
        assert density[0]["defects_per_kloc"] == pytest.approx(0.0)

    def test_large_line_count(self, benchmarks):
        """Handles large LOC values correctly."""
        run = benchmarks.start_run(approach="greymatter", task_name="Monolith")
        completed = benchmarks.complete_run(
            run["id"], lines=1_000_000, tests=5000, passing=5000, first_pass=True,
        )
        assert completed["lines_written"] == 1_000_000

    def test_all_tests_failing(self, benchmarks):
        """Run where no tests pass is tracked correctly."""
        run = benchmarks.start_run(approach="subagent", task_name="Broken")
        completed = benchmarks.complete_run(
            run["id"], lines=50, tests=10, passing=0, first_pass=False,
        )
        assert completed["tests_passing"] == 0
        assert completed["first_pass_success"] == 0

    def test_multiple_issues_same_category(self, benchmarks):
        """Multiple issues in the same category are tracked independently."""
        run = benchmarks.start_run(approach="subagent", task_name="Messy")
        benchmarks.complete_run(run["id"], lines=100, tests=3, passing=3, first_pass=False)

        ids = []
        for i in range(5):
            issue_id = benchmarks.record_issue(
                run["id"], severity="minor", category="naming",
                description=f"Bad name {i}", found_by="review",
            )
            ids.append(issue_id)

        issues = benchmarks.get_issues(run["id"])
        assert len(issues) == 5
        assert len(set(ids)) == 5  # All unique IDs

    def test_unresolved_issues_excluded_from_time_to_correct(self, benchmarks):
        """time_to_correct only considers resolved issues."""
        run = benchmarks.start_run(approach="greymatter", task_name="Partial fix")
        benchmarks.complete_run(run["id"], lines=100, tests=5, passing=5, first_pass=True)

        resolved = benchmarks.record_issue(run["id"], "minor", "naming", "Fixed", "review")
        benchmarks.record_issue(run["id"], "critical", "typing", "Unfixed", "review")
        benchmarks.resolve_issue(resolved, fix_minutes=10)

        ttc = benchmarks.time_to_correct()
        assert len(ttc) == 1
        assert ttc[0]["avg_fix_minutes"] == pytest.approx(10.0)

    def test_time_to_correct_no_resolved_issues(self, benchmarks):
        """time_to_correct with no resolved issues returns empty."""
        run = benchmarks.start_run(approach="greymatter", task_name="No fixes")
        benchmarks.complete_run(run["id"], lines=100, tests=5, passing=5, first_pass=True)
        benchmarks.record_issue(run["id"], "minor", "naming", "Unfixed", "review")

        ttc = benchmarks.time_to_correct()
        assert ttc == []


# ---------------------------------------------------------------------------
# Error cases
# ---------------------------------------------------------------------------


class TestErrorCases:
    """Input validation and error handling."""

    def test_invalid_approach_raises(self, benchmarks):
        """Starting a run with an invalid approach raises ValueError."""
        with pytest.raises(ValueError, match="Invalid approach"):
            benchmarks.start_run(approach="telepathy", task_name="Bad")

    def test_all_valid_approaches_accepted(self, benchmarks):
        """Every approach in VALID_APPROACHES can start a run."""
        for approach in VALID_APPROACHES:
            run = benchmarks.start_run(approach=approach, task_name=f"Test {approach}")
            assert run["approach"] == approach

    def test_invalid_severity_raises(self, benchmarks):
        """Recording an issue with invalid severity raises ValueError."""
        run = benchmarks.start_run(approach="greymatter", task_name="T1")
        benchmarks.complete_run(run["id"], lines=100, tests=5, passing=5, first_pass=True)

        with pytest.raises(ValueError, match="Invalid severity"):
            benchmarks.record_issue(
                run["id"], severity="catastrophic", category="style",
                description="Bad", found_by="review",
            )

    def test_all_valid_severities_accepted(self, benchmarks):
        """Every severity in VALID_SEVERITIES can be recorded."""
        run = benchmarks.start_run(approach="greymatter", task_name="T1")
        benchmarks.complete_run(run["id"], lines=100, tests=5, passing=5, first_pass=True)

        for severity in VALID_SEVERITIES:
            benchmarks.record_issue(
                run["id"], severity=severity, category="test",
                description=f"Issue with {severity}", found_by="review",
            )

        issues = benchmarks.get_issues(run["id"])
        assert len(issues) == len(VALID_SEVERITIES)

    def test_get_nonexistent_run(self, benchmarks):
        """Fetching a nonexistent run returns None."""
        result = benchmarks.get_run("br-nonexistent")
        assert result is None

    def test_get_scores_for_empty_run(self, benchmarks):
        """Fetching scores for a run with no scores returns empty list."""
        run = benchmarks.start_run(approach="greymatter", task_name="T1")
        scores = benchmarks.get_scores(run["id"])
        assert scores == []

    def test_get_issues_for_empty_run(self, benchmarks):
        """Fetching issues for a run with no issues returns empty list."""
        run = benchmarks.start_run(approach="greymatter", task_name="T1")
        issues = benchmarks.get_issues(run["id"])
        assert issues == []


# ---------------------------------------------------------------------------
# Database integrity
# ---------------------------------------------------------------------------


class TestDatabaseIntegrity:
    """Verify data persists and queries work across transactions."""

    def test_run_data_survives_requery(self, benchmarks):
        """Data written in one call is readable in the next."""
        run = benchmarks.start_run(approach="greymatter", task_name="Persist test")
        benchmarks.complete_run(run["id"], lines=42, tests=3, passing=3, first_pass=True)

        # Re-fetch from DB
        fetched = benchmarks.get_run(run["id"])
        assert fetched is not None
        assert fetched["lines_written"] == 42
        assert fetched["task_name"] == "Persist test"

    def test_unique_run_ids(self, benchmarks):
        """Each run gets a unique ID."""
        ids = set()
        for i in range(50):
            run = benchmarks.start_run(approach="greymatter", task_name=f"Run {i}")
            ids.add(run["id"])
        assert len(ids) == 50

    def test_transaction_rollback_on_error(self, db, benchmarks):
        """Failed transaction doesn't leave partial data."""
        run = benchmarks.start_run(approach="greymatter", task_name="Transaction test")
        benchmarks.complete_run(run["id"], lines=100, tests=5, passing=5, first_pass=True)

        try:
            with db.transaction():
                benchmarks.record_issue(
                    run["id"], "minor", "naming", "Will rollback", "review",
                )
                raise RuntimeError("Simulated failure")
        except RuntimeError:
            pass

        issues = benchmarks.get_issues(run["id"])
        assert issues == []

    def test_session_id_linkage(self, db, benchmarks):
        """Build runs can link to sessions via session_id."""
        db.execute(
            "INSERT INTO sessions (id, project) VALUES (?, ?)",
            ("test-session-001", "greymatter"),
        )
        run = benchmarks.start_run(
            approach="greymatter", task_name="Linked run",
            session_id="test-session-001",
        )
        fetched = benchmarks.get_run(run["id"])
        assert fetched["session_id"] == "test-session-001"
