import copy,json,unittest,sys
from pathlib import Path
HERE=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(HERE))
from gate_rules import validate_gate
RULES=json.loads((HERE/'CHECK_RULES.json').read_text(encoding='utf-8'))

def gate_from(path, ref):
    d=json.loads(path.read_text(encoding='utf-8'))
    g={'artifact_id':d['artifact_id'],'fixture_ref':ref,'virtual_cast':copy.deepcopy(d['virtual_cast']),'repair_agent':copy.deepcopy(d['repair_agent'])}
    for axis in ('virtual_cast','repair_agent'):
        g[axis]['decision']={'state':'PILOT','rationale':'bounded visible evidence','stop_or_pivot_condition':'Stop or pivot if decisive evidence or authority fails.','governance_triggers':[{'type':'REVIEW','condition':'Review when a governing assumption changes.'}]}
    return g

def baseline(): return gate_from(HERE/'fixtures/baseline_fixture.json','fixture://AWR008-BASELINE-01')

class GateRuleTests(unittest.TestCase):
    def codes(self,g,bind=False): return {e['code'] for e in validate_gate(g,RULES,HERE if bind else None)}
    def test_consistent_bound_baseline_pilot_passes(self): self.assertEqual([],validate_gate(baseline(),RULES,HERE))
    def test_unknown_fixture_rejected(self):
        g=baseline(); g['fixture_ref']='fixture://NOPE'; self.assertIn('E_FIXTURE_REF',self.codes(g,True))
    def test_fixture_field_fabrication_rejected(self):
        g=baseline(); g['virtual_cast']['identity_rights_owner']='FABRICATED'; self.assertIn('E_FIXTURE_BINDING',self.codes(g,True))
    def test_virtual_cast_positive_rejects_missing_rights_owner(self):
        g=baseline(); g['virtual_cast']['identity_rights_owner']=''; self.assertIn('E_VC_OWNER',self.codes(g))
    def test_virtual_cast_positive_rejects_missing_production_owner(self):
        g=baseline(); g['virtual_cast']['production_owner']=''; self.assertIn('E_VC_OWNER',self.codes(g))
    def test_virtual_cast_positive_rejects_render_budget_overrun(self):
        g=baseline(); g['virtual_cast']['cost_risk']['estimated_units']=140; self.assertIn('E_VC_BUDGET',self.codes(g))
    def test_repair_use_rejects_propose_only_scope(self):
        g=baseline(); g['repair_agent']['decision']['state']='USE'; g['repair_agent']['write_scope']='PROPOSE_ONLY'; self.assertIn('E_RA_USE_SCOPE',self.codes(g))
    def test_repair_positive_rejects_missing_tests(self):
        g=baseline(); g['repair_agent']['test_fixture']=''; self.assertIn('E_RA_CONTROL_EVIDENCE',self.codes(g))
    def test_repair_positive_rejects_missing_rollback(self):
        g=baseline(); g['repair_agent']['rollback_plan']=''; self.assertIn('E_RA_CONTROL_EVIDENCE',self.codes(g))
    def test_repair_positive_rejects_billing_or_production_target(self):
        g=baseline(); g['repair_agent']['repair_target']='BILLING_CODE_AND_PRODUCTION_DATA'; self.assertIn('E_RA_HIGH_RISK_TARGET',self.codes(g))
    def test_positive_repair_requires_boolean_approval_required(self):
        g=baseline(); del g['repair_agent']['approval_required']; self.assertIn('E_RA_APPROVAL_REQUIRED_TYPE',self.codes(g))
    def test_positive_repair_requires_configured_approval_when_required(self):
        g=baseline(); g['repair_agent']['approval_state']='MISSING'; self.assertIn('E_RA_APPROVAL',self.codes(g))
    def test_approval_false_requires_explicit_not_required_state(self):
        g=baseline(); g['repair_agent']['approval_required']=False; g['repair_agent']['approval_state']=''; self.assertIn('E_RA_APPROVAL',self.codes(g))
    def test_positive_repair_requires_pass_evaluation(self):
        g=baseline(); g['repair_agent']['evaluation_result']='FAIL'; self.assertIn('E_RA_EVAL',self.codes(g))
    def test_missing_decision_is_reported_without_answer_key(self):
        g=baseline(); del g['repair_agent']['decision']['state']; codes=self.codes(g); self.assertIn('E_RA_DECISION',codes); self.assertFalse(any(c.startswith('EXPECTED_') for c in codes))
    def test_each_axis_requires_stop_or_pivot(self):
        g=baseline(); g['virtual_cast']['decision']['stop_or_pivot_condition']=''; self.assertIn('E_VC_DECISION_META',self.codes(g))
    def test_each_axis_requires_applicable_governance_trigger(self):
        g=baseline(); g['virtual_cast']['decision']['governance_triggers']=[]; self.assertIn('E_VC_DECISION_META',self.codes(g))
    def test_visible_rules_are_actually_consumed(self):
        bad=copy.deepcopy(RULES); bad['rules']=[]; self.assertIn('E_RULESET',{e['code'] for e in validate_gate(baseline(),bad,None)})

    def _gate_for_bound_mutation(self, ref, vc_state):
        manifest=json.loads((HERE/'fixtures/FIXTURE_BINDINGS.json').read_text(encoding='utf-8'))
        spec=manifest['bindings'][ref]
        base=json.loads((HERE/spec['base']).read_text(encoding='utf-8'))
        mutation=json.loads((HERE/spec['path']).read_text(encoding='utf-8'))
        def merge(a,b):
            out=copy.deepcopy(a)
            for k,v in b.items():
                if isinstance(v,dict) and isinstance(out.get(k),dict): out[k]=merge(out[k],v)
                else: out[k]=copy.deepcopy(v)
            return out
        axis='virtual_cast' if mutation['axis']=='VIRTUAL_CAST' else 'repair_agent'
        base[axis]=merge(base[axis],mutation['changes']); base['fixture_id']=mutation['mutation_id']
        g={'artifact_id':base['artifact_id'],'fixture_ref':ref,'virtual_cast':copy.deepcopy(base['virtual_cast']),'repair_agent':copy.deepcopy(base['repair_agent'])}
        for name,state in [('virtual_cast',vc_state),('repair_agent','PILOT')]:
            g[name]['decision']={'state':state,'rationale':'bounded visible evidence','stop_or_pivot_condition':'Stop or pivot if decisive evidence or authority fails.','governance_triggers':[{'type':'REVIEW','condition':'Review when a governing assumption changes.'}]}
        return g

    def test_m1_use_rejects_missing_new_cast_owners(self):
        g=self._gate_for_bound_mutation('fixture://AWR008-M1','USE')
        self.assertIn('E_VC_OWNER',self.codes(g,True))

    def test_m1_pilot_rejects_missing_new_cast_owners(self):
        g=self._gate_for_bound_mutation('fixture://AWR008-M1','PILOT')
        self.assertIn('E_VC_OWNER',self.codes(g,True))

    def test_m1_owner_missing_typed_non_success_state_is_consistent(self):
        g=self._gate_for_bound_mutation('fixture://AWR008-M1','OWNER_MISSING')
        self.assertEqual([],validate_gate(g,RULES,HERE))

if __name__=='__main__': unittest.main(verbosity=2)
