import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock
from bot import detect
from bot.lib.wago import Build


class TestDetect(unittest.TestCase):
    def setUp(self):
        self.tmp = Path(tempfile.mkdtemp())
        self.state_dir = self.tmp / "state"
        self.state_dir.mkdir()
        self.reports_dir = self.tmp / "reports"
        self.reports_dir.mkdir()
        self.repo_root = self.tmp / "repo"
        self.repo_root.mkdir()

    def test_kill_switch_short_circuits(self):
        (self.state_dir / "disabled").touch()
        with patch("bot.detect.fetch_builds") as fb:
            rc = detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        fb.assert_not_called()
        self.assertEqual(rc, 0)

    def test_first_run_records_state_no_analysis(self):
        builds = [
            Build(branch="wow", version="11.2.7.110207", build=110207),
            Build(branch="wow_classic_era", version="1.15.8.11508", build=11508),
        ]
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer") as ra, \
             patch("bot.detect.notify") as nf:
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        ra.assert_not_called()
        nf.assert_not_called()
        saved = json.loads((self.state_dir / "last-seen-builds.json").read_text(encoding="utf-8"))
        self.assertEqual(saved["wow"]["build"], 110207)

    def test_new_build_triggers_analyzer_and_notification(self):
        # Stage prior state.
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        report_path = self.reports_dir / "fake.md"
        report_path.write_text("# fake\n", encoding="utf-8")
        from bot.lib.fixer import FixResult
        from bot.lib.verify import VerifyResult
        worktree = self.tmp / "wt_a"
        worktree.mkdir()
        fix = FixResult(branch="bot/x", worktree_path=worktree, commit_sha="a",
                        confidence=90, rationale="x", files_changed=[], claude_exit_code=0)
        vr = VerifyResult(passed_all=True, rows=[])
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(2, report_path, set())) as ra, \
             patch("bot.detect.propose_fix", return_value=fix), \
             patch("bot.detect.run_verify", return_value=vr), \
             patch("bot.detect.notify") as nf:
            rc = detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        ra.assert_called_once()
        nf.assert_called_once()
        self.assertEqual(rc, 0)
        saved = json.loads((self.state_dir / "last-seen-builds.json").read_text(encoding="utf-8"))
        self.assertEqual(saved["wow"]["build"], 110300)

    def test_zero_at_risk_does_not_notify(self):
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(0, self.reports_dir / "x.md", set())), \
             patch("bot.detect.notify") as nf:
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        nf.assert_not_called()

    def test_ptr_branch_logged_but_not_analyzed(self):
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wowt": {"build": 110200, "version": "11.2.0", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wowt", version="11.3.0.110301", build=110301)]
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer") as ra, \
             patch("bot.detect.notify") as nf:
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        ra.assert_not_called()
        nf.assert_not_called()
        saved = json.loads((self.state_dir / "last-seen-builds.json").read_text(encoding="utf-8"))
        self.assertEqual(saved["wowt"]["build"], 110301)

    def test_http_failure_increments_counter_does_not_crash(self):
        from bot.lib.http import HTTPError
        with patch("bot.detect.fetch_builds", side_effect=HTTPError("network")):
            rc = detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        self.assertEqual(rc, 0)
        self.assertEqual((self.state_dir / "failure-count.txt").read_text().strip(), "1")

    def test_notification_appends_log_line(self):
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        report_path = self.reports_dir / "fake.md"
        report_path.write_text("# fake\n", encoding="utf-8")
        from bot.lib.fixer import FixResult
        from bot.lib.verify import VerifyResult
        worktree = self.tmp / "wt_log"
        worktree.mkdir()
        fix = FixResult(branch="bot/x", worktree_path=worktree, commit_sha="a",
                        confidence=90, rationale="x", files_changed=[], claude_exit_code=0)
        vr = VerifyResult(passed_all=True, rows=[])
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(2, report_path, set())), \
             patch("bot.detect.propose_fix", return_value=fix), \
             patch("bot.detect.run_verify", return_value=vr), \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        log = (self.state_dir / "notifications.log").read_text(encoding="utf-8")
        self.assertIn("wow 11.3.0", log)
        self.assertIn(str(report_path), log)

    def test_zero_at_risk_does_not_log(self):
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(0, self.reports_dir / "x.md", set())), \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        self.assertFalse((self.state_dir / "notifications.log").exists())

    def test_more_than_8_at_risk_skips_fixer(self):
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        report_path = self.reports_dir / "fake.md"
        report_path.write_text("# fake\n", encoding="utf-8")
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(9, report_path, {"X"})), \
             patch("bot.detect.propose_fix") as pf, \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        pf.assert_not_called()

    def test_within_cap_runs_fixer_and_records_pending_fix(self):
        from bot.lib.fixer import FixResult
        from bot.lib.verify import VerifyResult, VerifyRow
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        report_path = self.reports_dir / "fake.md"
        report_path.write_text("# fake\n", encoding="utf-8")
        worktree = self.tmp / "wt_within"
        worktree.mkdir()

        fix = FixResult(
            branch="bot/patch-wow-11_3_0",
            worktree_path=worktree,
            commit_sha="deadbeef",
            confidence=88,
            rationale="renamed",
            files_changed=["CritMatic.lua"],
            claude_exit_code=0,
        )
        vr = VerifyResult(passed_all=True, rows=[VerifyRow("luac -p", True, "ok")])
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(2, report_path, {"X"})), \
             patch("bot.detect.propose_fix", return_value=fix), \
             patch("bot.detect.run_verify", return_value=vr), \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        from bot.lib import state as state_mod
        fixes = state_mod.load_pending_fixes(self.state_dir)
        self.assertIn(fix.branch, fixes)
        self.assertEqual(fixes[fix.branch]["status"], "auto-ship-eligible")

    def test_low_confidence_or_failed_verify_yields_held(self):
        from bot.lib.fixer import FixResult
        from bot.lib.verify import VerifyResult, VerifyRow
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        report_path = self.reports_dir / "fake.md"
        report_path.write_text("# fake\n", encoding="utf-8")
        worktree = self.tmp / "wt_low"
        worktree.mkdir()

        fix = FixResult(
            branch="bot/patch-wow-11_3_0",
            worktree_path=worktree,
            commit_sha="abc",
            confidence=60,  # below threshold
            rationale="unsure",
            files_changed=["CritMatic.lua"],
            claude_exit_code=0,
        )
        vr = VerifyResult(passed_all=True, rows=[VerifyRow("luac -p", True, "ok")])
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(2, report_path, {"X"})), \
             patch("bot.detect.propose_fix", return_value=fix), \
             patch("bot.detect.run_verify", return_value=vr), \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        from bot.lib import state as state_mod
        fixes = state_mod.load_pending_fixes(self.state_dir)
        self.assertEqual(fixes[fix.branch]["status"], "held-for-review")

    def test_fixer_exception_does_not_crash_detector(self):
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        report_path = self.reports_dir / "fake.md"
        report_path.write_text("# fake\n", encoding="utf-8")
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(2, report_path, {"X"})), \
             patch("bot.detect.propose_fix", side_effect=RuntimeError("worktree failed")), \
             patch("bot.detect.notify"):
            rc = detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        self.assertEqual(rc, 0)

    def test_drain_fires_release_when_due(self):
        from bot.lib.release import ReleaseResult
        from bot.lib import state as state_mod
        state_mod.record_pending_release(self.state_dir, "v5.5.8", {
            "branch": "wow", "tag": "v5.5.8",
            "scheduled_at": "2020-01-01T00:00:00+00:00",
            "status": "scheduled", "fix_branch": "bot/x",
            "report_path": "bot/reports/x.md", "created_at": "t", "retries": 0,
        })
        with patch("bot.detect.fetch_builds", return_value=[]), \
             patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="", stderr="")) as sp, \
             patch("bot.detect.run_release", return_value=ReleaseResult(success=True, stdout="ok", stderr="", returncode=0)) as rr, \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        rr.assert_called_once()
        push_calls = [c for c in sp.call_args_list if c.args and "push" in c.args[0]]
        self.assertTrue(len(push_calls) >= 1)
        self.assertNotIn("v5.5.8", state_mod.load_pending_releases(self.state_dir))

    def test_drain_does_not_fire_when_not_due(self):
        from bot.lib import state as state_mod
        state_mod.record_pending_release(self.state_dir, "v5.5.9", {
            "branch": "wow", "tag": "v5.5.9",
            "scheduled_at": "2099-01-01T00:00:00+00:00",
            "status": "scheduled", "fix_branch": "bot/x",
            "report_path": "bot/reports/x.md", "created_at": "t", "retries": 0,
        })
        with patch("bot.detect.fetch_builds", return_value=[]), \
             patch("bot.detect.run_release") as rr, \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        rr.assert_not_called()
        self.assertIn("v5.5.9", state_mod.load_pending_releases(self.state_dir))

    def test_drain_skipped_when_auto_release_disabled(self):
        from bot.lib import state as state_mod
        (self.state_dir / "auto-release-disabled").touch()
        state_mod.record_pending_release(self.state_dir, "v5.5.9", {
            "branch": "wow", "tag": "v5.5.9",
            "scheduled_at": "2020-01-01T00:00:00+00:00",
            "status": "scheduled", "fix_branch": "bot/x",
            "report_path": "bot/reports/x.md", "created_at": "t", "retries": 0,
        })
        with patch("bot.detect.fetch_builds", return_value=[]), \
             patch("bot.detect.run_release") as rr, \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        rr.assert_not_called()
        self.assertIn("v5.5.9", state_mod.load_pending_releases(self.state_dir))

    def test_drain_increments_retries_on_release_failure(self):
        from bot.lib.release import ReleaseResult
        from bot.lib import state as state_mod
        state_mod.record_pending_release(self.state_dir, "v5.5.9", {
            "branch": "wow", "tag": "v5.5.9",
            "scheduled_at": "2020-01-01T00:00:00+00:00",
            "status": "scheduled", "fix_branch": "bot/x",
            "report_path": "bot/reports/x.md", "created_at": "t", "retries": 0,
        })
        with patch("bot.detect.fetch_builds", return_value=[]), \
             patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="", stderr="")), \
             patch("bot.detect.run_release", return_value=ReleaseResult(success=False, stdout="", stderr="boom", returncode=1)), \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        entry = state_mod.load_pending_releases(self.state_dir)["v5.5.9"]
        self.assertEqual(entry["retries"], 1)

    def test_drain_stops_attempting_at_retry_cap(self):
        from bot.lib import state as state_mod
        # Entry already at the retry cap.
        state_mod.record_pending_release(self.state_dir, "v5.5.9", {
            "branch": "wow", "tag": "v5.5.9",
            "scheduled_at": "2020-01-01T00:00:00+00:00",
            "status": "scheduled", "fix_branch": "bot/x",
            "report_path": "bot/reports/x.md", "created_at": "t",
            "retries": detect.RELEASE_RETRY_CAP,
        })
        with patch("bot.detect.fetch_builds", return_value=[]), \
             patch("subprocess.run") as sp, \
             patch("bot.detect.run_release") as rr, \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        # No push attempt and no release attempt; entry preserved unchanged.
        push_calls = [c for c in sp.call_args_list if c.args and "push" in c.args[0]]
        self.assertEqual(push_calls, [])
        rr.assert_not_called()
        entry = state_mod.load_pending_releases(self.state_dir)["v5.5.9"]
        self.assertEqual(entry["retries"], detect.RELEASE_RETRY_CAP)

    def test_auto_release_pipeline_records_pending_release(self):
        from bot.lib.fixer import FixResult
        from bot.lib.verify import VerifyResult, VerifyRow
        from bot.lib import state as state_mod
        (self.repo_root / "CritMatic.toc").write_text("## Version: 5.5.7\n", encoding="utf-8")
        (self.repo_root / "versions.json").write_text('{"critmatic_version": "5.5.7"}\n', encoding="utf-8")
        (self.repo_root / "CHANGE_LOG.md").write_text("# Changelog\n\n", encoding="utf-8")
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        report_path = self.reports_dir / "fake.md"
        report_path.write_text("# fake\n", encoding="utf-8")
        worktree = self.tmp / "wt_pipeline"
        worktree.mkdir()
        fix = FixResult(branch="bot/patch-wow-11_3_0", worktree_path=worktree, commit_sha="abc",
                        confidence=92, rationale="renamed", files_changed=["CritMatic.lua"], claude_exit_code=0)
        vr = VerifyResult(passed_all=True, rows=[VerifyRow("luac -p", True, "ok")])
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(2, report_path, {"X"})), \
             patch("bot.detect.propose_fix", return_value=fix), \
             patch("bot.detect.run_verify", return_value=vr), \
             patch("bot.detect.cmd_merge", return_value=(0, "merged")), \
             patch("bot.detect.apply_bump", return_value="v5.5.8"), \
             patch("bot.detect._count_diff_lines", return_value=10), \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        releases = state_mod.load_pending_releases(self.state_dir)
        self.assertEqual(len(releases), 1)
        rel = next(iter(releases.values()))
        self.assertEqual(rel["tag"], "v5.5.8")
        self.assertEqual(rel["status"], "scheduled")
        self.assertEqual(rel["retries"], 0)

    def test_auto_release_pipeline_falls_back_to_phase2_when_killswitched(self):
        from bot.lib.fixer import FixResult
        from bot.lib.verify import VerifyResult, VerifyRow
        from bot.lib import state as state_mod
        (self.state_dir / "auto-release-disabled").touch()
        (self.state_dir / "last-seen-builds.json").write_text(json.dumps({
            "wow": {"build": 110207, "version": "11.2.7", "seen_at": "2026-04-01T00:00:00Z"},
        }), encoding="utf-8")
        builds = [Build(branch="wow", version="11.3.0.110300", build=110300)]
        report_path = self.reports_dir / "fake.md"
        report_path.write_text("# fake\n", encoding="utf-8")
        worktree = self.tmp / "wt_kill"
        worktree.mkdir()
        fix = FixResult(branch="bot/patch-wow-11_3_0", worktree_path=worktree, commit_sha="abc",
                        confidence=92, rationale="x", files_changed=[], claude_exit_code=0)
        vr = VerifyResult(passed_all=True, rows=[VerifyRow("luac -p", True, "ok")])
        with patch("bot.detect.fetch_builds", return_value=builds), \
             patch("bot.detect.run_analyzer", return_value=(2, report_path, {"X"})), \
             patch("bot.detect.propose_fix", return_value=fix), \
             patch("bot.detect.run_verify", return_value=vr), \
             patch("bot.detect.cmd_merge") as cm, \
             patch("bot.detect.apply_bump") as ab, \
             patch("bot.detect.notify"):
            detect.run(state_dir=self.state_dir, reports_dir=self.reports_dir, repo_root=self.repo_root)
        cm.assert_not_called()
        ab.assert_not_called()
        fixes = state_mod.load_pending_fixes(self.state_dir)
        self.assertIn(fix.branch, fixes)
        self.assertEqual(fixes[fix.branch]["status"], "auto-ship-eligible")
        self.assertEqual(state_mod.load_pending_releases(self.state_dir), {})


if __name__ == "__main__":
    unittest.main()
