"""
Tests for Phase 22: Oroboros Protocol — self-building infrastructure.
"""

import json
import tempfile
from pathlib import Path

import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all storage to temp directory."""
    channels = tmp_path / "channels"
    sessions = tmp_path / "sessions"
    cursors = tmp_path / "cursors"
    tasks = tmp_path / "tasks"
    oroboros = tmp_path / "oroboros"
    cycles = oroboros / "cycles"

    for d in (channels, sessions, cursors, tasks, oroboros, cycles):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(srv, "CHANNELS_DIR", channels)
    monkeypatch.setattr(srv, "SESSIONS_DIR", sessions)
    monkeypatch.setattr(srv, "CURSORS_DIR", cursors)

    import brynq.tasks as t
    monkeypatch.setattr(t, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(t, "TASKS_DIR", tasks)
    monkeypatch.setattr(t, "CHANNELS_DIR", channels)

    import brynq.oroboros as o
    monkeypatch.setattr(o, "OROBOROS_DIR", oroboros)
    monkeypatch.setattr(o, "CYCLES_DIR", cycles)
    monkeypatch.setattr(o, "LEDGER_FILE", oroboros / "ledger.jsonl")
    monkeypatch.setattr(o, "SCAN_FILE", oroboros / "scan_results.json")
    monkeypatch.setattr(o, "CHANNELS_DIR", channels)


class TestScanCodebase:
    def test_scan_returns_report(self):
        from brynq.oroboros import scan_codebase
        report = scan_codebase("src/brynq")
        assert "scan_id" in report
        assert "findings" in report
        assert "total_modules" in report
        assert report["total_modules"] > 0

    def test_scan_nonexistent_dir(self, tmp_path):
        from brynq.oroboros import scan_codebase
        report = scan_codebase(str(tmp_path / "does_not_exist_xyz"))
        assert "error" in report

    def test_scan_finds_categories(self):
        from brynq.oroboros import scan_codebase
        report = scan_codebase("src/brynq")
        assert isinstance(report["findings_by_category"], dict)

    def test_get_latest_scan(self):
        from brynq.oroboros import scan_codebase, get_latest_scan
        scan_codebase("src/brynq")
        latest = get_latest_scan()
        assert latest is not None
        assert "scan_id" in latest

    def test_get_latest_scan_none(self):
        from brynq.oroboros import get_latest_scan
        assert get_latest_scan() is None


class TestImprovementCycles:
    def _make_finding(self):
        return {
            "category": "missing_tests",
            "severity": "medium",
            "target": "src/brynq/foo.py",
            "description": "Module foo has no tests",
            "suggested_task": "Write tests for brynq.foo",
            "skills_required": ["python", "testing"],
        }

    def test_propose_improvement(self):
        from brynq.oroboros import propose_improvement
        cycle = propose_improvement(self._make_finding())
        assert cycle["cycle_id"]
        assert cycle["status"] == "proposed"
        assert cycle["category"] == "missing_tests"

    def test_propose_with_custom_title(self):
        from brynq.oroboros import propose_improvement
        cycle = propose_improvement(self._make_finding(), title="Custom task")
        assert cycle["title"] == "Custom task"

    def test_post_improvement_task(self):
        from brynq.oroboros import propose_improvement, post_improvement_task
        cycle = propose_improvement(self._make_finding())
        result = post_improvement_task(cycle["cycle_id"])
        assert result["status"] == "posted"
        assert result["task_id"]

    def test_post_nonexistent_cycle(self):
        from brynq.oroboros import post_improvement_task
        result = post_improvement_task("nope")
        assert "error" in result

    def test_post_already_posted(self):
        from brynq.oroboros import propose_improvement, post_improvement_task
        cycle = propose_improvement(self._make_finding())
        post_improvement_task(cycle["cycle_id"])
        result = post_improvement_task(cycle["cycle_id"])
        assert "error" in result

    def test_claim_improvement(self):
        from brynq.oroboros import propose_improvement, post_improvement_task, claim_improvement
        cycle = propose_improvement(self._make_finding())
        post_improvement_task(cycle["cycle_id"])
        result = claim_improvement(cycle["cycle_id"], agent_name="builder@claude")
        assert result["status"] == "claimed"
        assert result["assigned_to"] == "builder@claude"

    def test_claim_not_posted(self):
        from brynq.oroboros import propose_improvement, claim_improvement
        cycle = propose_improvement(self._make_finding())
        result = claim_improvement(cycle["cycle_id"])
        assert "error" in result

    def test_submit_for_review(self):
        from brynq.oroboros import (
            propose_improvement, post_improvement_task,
            claim_improvement, submit_for_review,
        )
        cycle = propose_improvement(self._make_finding())
        post_improvement_task(cycle["cycle_id"])
        claim_improvement(cycle["cycle_id"])
        result = submit_for_review(cycle["cycle_id"], "Added 15 tests, all passing")
        assert result["status"] == "in_review"

    def test_review_approved(self):
        from brynq.oroboros import (
            propose_improvement, post_improvement_task,
            claim_improvement, submit_for_review, review_improvement, get_cycle,
        )
        cycle = propose_improvement(self._make_finding())
        post_improvement_task(cycle["cycle_id"])
        claim_improvement(cycle["cycle_id"], agent_name="builder@claude")
        submit_for_review(cycle["cycle_id"], "Done")
        result = review_improvement(
            cycle["cycle_id"], approved=True,
            notes="LGTM", reviewer_name="reviewer@claude",
        )
        assert result["status"] == "approved"

    def test_review_rejected(self):
        from brynq.oroboros import (
            propose_improvement, post_improvement_task,
            claim_improvement, submit_for_review, review_improvement,
        )
        cycle = propose_improvement(self._make_finding())
        post_improvement_task(cycle["cycle_id"])
        claim_improvement(cycle["cycle_id"], agent_name="builder@claude")
        submit_for_review(cycle["cycle_id"], "Done")
        result = review_improvement(
            cycle["cycle_id"], approved=False,
            notes="Tests incomplete", reviewer_name="reviewer@claude",
        )
        assert result["status"] == "rejected"

    def test_no_self_review(self):
        from brynq.oroboros import (
            propose_improvement, post_improvement_task,
            claim_improvement, submit_for_review, review_improvement,
        )
        cycle = propose_improvement(self._make_finding())
        post_improvement_task(cycle["cycle_id"])
        claim_improvement(cycle["cycle_id"], agent_name="same-agent")
        submit_for_review(cycle["cycle_id"], "Done")
        result = review_improvement(
            cycle["cycle_id"], approved=True, reviewer_name="same-agent",
        )
        assert "error" in result
        assert "self-approve" in result["error"].lower() or "own work" in result["error"].lower()

    def test_complete_improvement(self):
        from brynq.oroboros import (
            propose_improvement, post_improvement_task,
            claim_improvement, submit_for_review,
            review_improvement, complete_improvement, get_cycle,
        )
        cycle = propose_improvement(self._make_finding())
        post_improvement_task(cycle["cycle_id"])
        claim_improvement(cycle["cycle_id"], agent_name="builder@claude")
        submit_for_review(cycle["cycle_id"], "Done")
        review_improvement(cycle["cycle_id"], approved=True, reviewer_name="reviewer@claude")
        result = complete_improvement(cycle["cycle_id"])
        assert result["status"] == "completed"
        c = get_cycle(cycle["cycle_id"])
        assert c["completed_at"] is not None

    def test_complete_not_approved(self):
        from brynq.oroboros import propose_improvement, complete_improvement
        cycle = propose_improvement(self._make_finding())
        result = complete_improvement(cycle["cycle_id"])
        assert "error" in result

    def test_veto_improvement(self):
        from brynq.oroboros import (
            propose_improvement, post_improvement_task, veto_improvement, get_cycle,
        )
        cycle = propose_improvement(self._make_finding())
        post_improvement_task(cycle["cycle_id"])
        result = veto_improvement(cycle["cycle_id"], "Not aligned with roadmap")
        assert result["status"] == "vetoed"
        c = get_cycle(cycle["cycle_id"])
        assert c["veto_reason"] == "Not aligned with roadmap"


class TestCycleQueries:
    def _create_cycles(self, n=3):
        from brynq.oroboros import propose_improvement
        finding = {
            "category": "missing_tests",
            "severity": "medium",
            "target": "src/brynq/x.py",
            "description": "test",
            "suggested_task": "test task",
            "skills_required": ["python"],
        }
        return [propose_improvement(finding) for _ in range(n)]

    def test_list_cycles(self):
        from brynq.oroboros import list_cycles
        self._create_cycles(3)
        cycles = list_cycles()
        assert len(cycles) == 3

    def test_list_cycles_filter_status(self):
        from brynq.oroboros import list_cycles, post_improvement_task
        cycles = self._create_cycles(3)
        post_improvement_task(cycles[0]["cycle_id"])
        posted = list_cycles(status="posted")
        assert len(posted) == 1

    def test_list_cycles_limit(self):
        from brynq.oroboros import list_cycles
        self._create_cycles(5)
        limited = list_cycles(limit=2)
        assert len(limited) == 2

    def test_get_cycle(self):
        from brynq.oroboros import get_cycle
        cycles = self._create_cycles(1)
        c = get_cycle(cycles[0]["cycle_id"])
        assert c is not None
        assert c["status"] == "proposed"

    def test_get_nonexistent_cycle(self):
        from brynq.oroboros import get_cycle
        assert get_cycle("nope") is None


class TestEvolutionStats:
    def test_empty_stats(self):
        from brynq.oroboros import get_evolution_stats
        stats = get_evolution_stats()
        assert stats["total_cycles"] == 0
        assert stats["completion_rate"] == 0.0

    def test_stats_with_cycles(self):
        from brynq.oroboros import (
            propose_improvement, post_improvement_task,
            claim_improvement, submit_for_review,
            review_improvement, complete_improvement,
            get_evolution_stats,
        )
        finding = {
            "category": "refactoring",
            "severity": "low",
            "target": "x.py",
            "description": "test",
            "suggested_task": "task",
            "skills_required": ["python"],
        }
        # Complete one cycle
        c = propose_improvement(finding)
        post_improvement_task(c["cycle_id"])
        claim_improvement(c["cycle_id"], agent_name="b")
        submit_for_review(c["cycle_id"], "done")
        review_improvement(c["cycle_id"], approved=True, reviewer_name="r")
        complete_improvement(c["cycle_id"])

        # Leave one proposed
        propose_improvement(finding)

        stats = get_evolution_stats()
        assert stats["total_cycles"] == 2
        assert stats["by_status"]["completed"] == 1
        assert stats["by_status"]["proposed"] == 1
        assert stats["completion_rate"] == 0.5

    def test_evolution_ledger(self):
        from brynq.oroboros import (
            propose_improvement, post_improvement_task,
            get_evolution_ledger,
        )
        finding = {
            "category": "feature_gap",
            "severity": "high",
            "target": "x.py",
            "description": "test",
            "suggested_task": "task",
            "skills_required": ["python"],
        }
        c = propose_improvement(finding)
        post_improvement_task(c["cycle_id"])
        ledger = get_evolution_ledger()
        assert len(ledger) >= 1
        assert ledger[-1]["event"] == "posted"

    def test_empty_ledger(self):
        from brynq.oroboros import get_evolution_ledger
        assert get_evolution_ledger() == []


class TestAutoOroboros:
    def test_run_cycle(self):
        from brynq.oroboros import run_oroboros_cycle
        result = run_oroboros_cycle("src/brynq", max_tasks=2, min_severity="low")
        assert "tasks_posted" in result
        assert result["tasks_posted"] <= 2

    def test_run_cycle_nonexistent_dir(self):
        from brynq.oroboros import run_oroboros_cycle
        result = run_oroboros_cycle("/fake/dir")
        assert "error" in result
