"""Tests for the master-parity staleness gate (#420).

Covers the pure assessment logic, the mutation-block helper, the env escape
hatches, and the server-side wiring: reads are never blocked by staleness, a
mutation is refused when the on-disk master has advanced, and the read-only
gitea_assess_master_parity tool reports the stale state.
"""
import os
import sys
import unittest
from unittest.mock import patch

sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))

import master_parity_gate as mp  # noqa: E402


SHA_A = "a" * 40
SHA_B = "b" * 40


class TestAssessMasterParity(unittest.TestCase):
    def test_in_parity_when_heads_match(self):
        res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_A)
        self.assertTrue(res["in_parity"])
        self.assertFalse(res["stale"])
        self.assertFalse(res["restart_required"])
        self.assertTrue(res["determinable"])

    def test_stale_when_master_advanced(self):
        res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_B)
        self.assertFalse(res["in_parity"])
        self.assertTrue(res["stale"])
        self.assertTrue(res["restart_required"])
        self.assertTrue(res["determinable"])
        self.assertTrue(any("restart" in r for r in res["reasons"]))

    def test_missing_startup_head_not_determinable(self):
        res = mp.assess_master_parity({"startup_head": None}, SHA_B)
        self.assertFalse(res["determinable"])
        self.assertFalse(res["stale"])
        self.assertTrue(res["in_parity"])

    def test_missing_current_head_not_determinable(self):
        res = mp.assess_master_parity({"startup_head": SHA_A}, None)
        self.assertFalse(res["determinable"])
        self.assertFalse(res["stale"])
        self.assertTrue(res["in_parity"])

    def test_none_startup_baseline(self):
        res = mp.assess_master_parity(None, SHA_A)
        self.assertFalse(res["determinable"])
        self.assertFalse(res["stale"])


class TestBlockReasonsAndReport(unittest.TestCase):
    def test_stale_produces_block_reasons(self):
        res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_B)
        self.assertTrue(mp.parity_block_reasons(res))

    def test_in_parity_no_block_reasons(self):
        res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_A)
        self.assertEqual(mp.parity_block_reasons(res), [])

    def test_disable_env_suppresses_block(self):
        res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_B)
        with patch.dict(os.environ, {mp.ENV_DISABLE: "1"}):
            self.assertTrue(mp.gate_disabled())
            self.assertEqual(mp.parity_block_reasons(res), [])

    def test_report_shape_when_stale(self):
        res = mp.assess_master_parity({"startup_head": SHA_A}, SHA_B)
        report = mp.parity_report(res)
        self.assertEqual(report["kind"], "server_stale")
        self.assertTrue(report["restart_required"])
        self.assertEqual(report["startup_head"], SHA_A)
        self.assertEqual(report["current_head"], SHA_B)
        self.assertTrue(report["recovery"])


class TestReadGitHead(unittest.TestCase):
    def test_test_override_takes_precedence(self):
        with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B}):
            self.assertEqual(mp.read_git_head("/nonexistent"), SHA_B)

    def test_blank_override_is_none(self):
        with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: "   "}):
            self.assertIsNone(mp.read_git_head("/nonexistent"))

    def test_empty_root_is_none(self):
        # No override set; empty root cannot resolve a HEAD.
        env = {k: v for k, v in os.environ.items()
               if k != mp.ENV_TEST_CURRENT_HEAD}
        with patch.dict(os.environ, env, clear=True):
            self.assertIsNone(mp.read_git_head(""))


class TestServerWiring(unittest.TestCase):
    """Integration with the gate choke point in the server namespace."""

    def setUp(self):
        import mcp_server
        self.srv = mcp_server
        # Pin a known startup baseline so the current-HEAD override can diverge.
        self._saved = self.srv._STARTUP_PARITY
        self.srv._STARTUP_PARITY = {"root": self.srv.PROJECT_ROOT,
                                    "startup_head": SHA_A}

    def tearDown(self):
        self.srv._STARTUP_PARITY = self._saved

    def test_reads_not_blocked_when_stale(self):
        with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B}):
            self.assertEqual(self.srv._master_parity_block("gitea.read"), [])

    def test_mutation_blocked_when_stale(self):
        with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B}):
            reasons = self.srv._master_parity_block("gitea.pr.create")
            self.assertTrue(reasons)
            # The profile-operation choke point surfaces the same block.
            self.assertTrue(self.srv._profile_operation_gate("gitea.pr.create"))

    def test_mutation_allowed_when_in_parity(self):
        with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_A}):
            self.assertEqual(
                self.srv._master_parity_block("gitea.pr.create"), [])

    def test_disable_env_lets_mutation_through(self):
        with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B,
                                     mp.ENV_DISABLE: "1"}):
            self.assertEqual(
                self.srv._master_parity_block("gitea.pr.create"), [])

    def test_assess_tool_reports_stale(self):
        with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_B}):
            out = self.srv.gitea_assess_master_parity(remote="prgs")
            self.assertTrue(out["stale"])
            self.assertTrue(out["restart_required"])
            self.assertEqual(out["startup_head"], SHA_A)
            self.assertEqual(out["current_head"], SHA_B)
            self.assertIn("report", out)

    def test_assess_tool_reports_in_parity(self):
        with patch.dict(os.environ, {mp.ENV_TEST_CURRENT_HEAD: SHA_A}):
            out = self.srv.gitea_assess_master_parity(remote="prgs")
            self.assertFalse(out["stale"])
            self.assertTrue(out["in_parity"])
            self.assertNotIn("report", out)


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