#!/usr/bin/env python3
"""Mutation/negative tests for the local OrgDef validator. Run python -m unittest discover -s governance/tests -v."""
import copy
import json
import sys
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from validate_orgdef import ValidationError, validate

BASE = json.loads((ROOT / "orgdef.json").read_text(encoding="utf-8"))

class OrgDefMutations(unittest.TestCase):
    def setUp(self):
        self.org = copy.deepcopy(BASE)

    def rejects(self, expected):
        with self.assertRaisesRegex(ValidationError, expected):
            validate(self.org)

    def test_real_draft_passes(self):
        self.assertEqual(validate(self.org)["state"], "proposed")

    def test_dangling_edge(self):
        self.org["relationships"].append({"type":"reports_to","from":"ghost","to":"principal"})
        self.rejects("dangling")

    def test_cycle(self):
        self.org["relationships"].append({"type":"reports_to","from":"principal","to":"labs-qa"})
        self.rejects("principal may not report")

    def test_cycle_below_principal(self):
        self.org["relationships"].append({"type":"reports_to","from":"strategist","to":"labs-product-strategist"})
        self.rejects("cycle")

    def test_unreachable_position(self):
        self.org["relationships"]=[x for x in self.org["relationships"] if not (x["type"]=="reports_to" and x["from"]=="labs-release")]
        self.rejects("cannot reach principal")

    def test_self_validation(self):
        self.org["relationships"].append({"type":"validates_for","from":"labs-implementer","to":"labs-product-strategist"})
        self.rejects("reviewer in target chain")

    def test_same_manager_invalidates_independence(self):
        self.org["relationships"].append({"type":"reports_to","from":"labs-qa","to":"labs-product-strategist"})
        self.rejects("reviewer shares target reporting manager")

    def test_vacant_with_incumbent(self):
        self.org["items"][2]["incumbent"]="model-unknown"
        self.rejects("vacant seat has incumbent")

    def test_staffed_without_record(self):
        self.org["items"][2].update(status="staffed",incumbent="model")
        self.rejects("staffed seat lacks appointment record")

    def test_staffed_with_self_attested_record_structurally_valid_only(self):
        with tempfile.TemporaryDirectory() as tmp:
            base=Path(tmp)
            (base / "governance/appointments").mkdir(parents=True)
            path="governance/appointments/test.json"
            (base / path).write_text(json.dumps({"seat":"institute-researcher","approved_by":"principal",
                "approved_at":"2026-11-07","session_binding":"session-1"}),encoding="utf-8")
            existing = ROOT / "appointments/2026-11-07--strategist--appointment.json"
            (base / "governance/appointments/2026-11-07--strategist--appointment.json").write_text(
                existing.read_text(encoding="utf-8"), encoding="utf-8")
            self.org["items"][2].update(status="staffed",incumbent="a-model",appointment_record=path)
            self.assertEqual(validate(self.org, base/"governance")["state"],"proposed")

    def test_adoption_without_record(self):
        self.org["status"]="adopted"
        self.rejects("adoption record")

    def test_self_attested_adoption_structurally_valid_only(self):
        with tempfile.TemporaryDirectory() as tmp:
            base=Path(tmp)
            # An adopted organization must also resolve any already-staffed seat record.
            (base / "governance/appointments").mkdir(parents=True)
            existing = ROOT / "appointments/2026-11-07--strategist--appointment.json"
            (base / "governance/appointments/2026-11-07--strategist--appointment.json").write_text(
                existing.read_text(encoding="utf-8"), encoding="utf-8")
            (base / "governance/adoptions").mkdir(parents=True)
            path="governance/adoptions/test.json"
            (base / path).write_text(json.dumps({"version":self.org["version"],"approved_by":"principal",
                "approved_at":"2026-11-07","review_memo":"memos/review.openthing"}),encoding="utf-8")
            self.org["status"]="adopted"
            self.org["adoption_record"]=path
            self.assertEqual(validate(self.org, base/"governance")["state"],"adopted")

    def test_blank_values(self):
        self.org["values"]=[{"description":"  "}] * 3
        self.rejects("meaningful entries")

    def test_missing_employer_red_line(self):
        self.org["red_lines"]=[x for x in self.org["red_lines"] if "Edsby" not in x["rule"]]
        self.rejects("employer confidentiality")

    def test_missing_cross_division_review(self):
        self.org["relationships"]=[e for e in self.org["relationships"] if not(e["type"]=="validates_for" and e["from"]=="institute-reviewer" and e["to"]=="labs-implementer")]
        self.rejects("cross-division")


    def test_missing_limitations(self):
        self.org.pop("limitations")
        self.rejects("governance limitations")

    def test_blank_limitations(self):
        self.org["limitations"] = ["   "] * 4
        self.rejects("governance limitations")

    def test_missing_publication_policy(self):
        self.org.pop("publication_policy")
        self.rejects("publication policy")

    def test_missing_authorship_classes(self):
        self.org["publication_policy"]["authorship_classes"] = []
        self.rejects("authorship classes")

    def test_missing_publication_restrictions(self):
        self.org["publication_policy"]["restricted_data"] = []
        self.rejects("publication restrictions")

    def test_missing_publication_metadata(self):
        self.org["publication_policy"]["required_metadata"] = []
        self.rejects("publication metadata")

    def test_missing_record_conventions(self):
        self.org.pop("records")
        self.rejects("governance record conventions")

    def test_blank_record_conventions(self):
        self.org["records"]["review"] = "  "
        self.rejects("governance record conventions")

    def test_missing_release_review(self):
        self.org["relationships"] = [e for e in self.org["relationships"]
            if not (e["type"] == "validates_for" and e["from"] == "labs-qa"
                    and e["to"] == "labs-release")]
        self.rejects("independent release validation")

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