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 decision(state='PILOT'):
    return {'state':state,'rationale':'bounded visible evidence','stop_or_pivot_condition':'Stop or pivot if decisive evidence or authority changes.','governance_triggers':[{'type':'REVIEW','condition':'Review when a governing assumption changes.'}]}

def gate_from(path,ref):
    d=json.loads(path.read_text(encoding='utf-8'))
    g={'artifact_id':d['artifact_id'],'fixture_ref':ref,'multi_region':copy.deepcopy(d['multi_region']),'ad_automation':copy.deepcopy(d['ad_automation'])}
    g['multi_region']['decision']=decision(); g['ad_automation']['decision']=decision(); return g

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

def bound_mutation(ref,mr_state='NOT_NOW',ad_state='PILOT'):
    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')); mut=json.loads((HERE/spec['path']).read_text(encoding='utf-8'))
    def merge(a,b):
        out=copy.deepcopy(a)
        for k,v in b.items(): out[k]=merge(out[k],v) if isinstance(v,dict) and isinstance(out.get(k),dict) else copy.deepcopy(v)
        return out
    axis='multi_region' if mut['axis']=='MULTI_REGION' else 'ad_automation'; base[axis]=merge(base[axis],mut['changes']); base['fixture_id']=mut['mutation_id']
    g={'artifact_id':base['artifact_id'],'fixture_ref':ref,'multi_region':copy.deepcopy(base['multi_region']),'ad_automation':copy.deepcopy(base['ad_automation'])}
    g['multi_region']['decision']=decision(mr_state); g['ad_automation']['decision']=decision(ad_state); return g

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_baseline_multi_region_not_now_and_ad_pilot_is_consistent(self):
        g=baseline(); g['multi_region']['decision']=decision('NOT_NOW'); self.assertEqual([],validate_gate(g,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['multi_region']['failover_owner']='FABRICATED'; self.assertIn('E_FIXTURE_BINDING',self.codes(g,True))
    def test_baseline_no_new_requirement_rejects_positive_mr(self):
        g=baseline(); self.assertIn('E_MR_TRIGGER',self.codes(g,True))
    def test_user_count_growth_without_requirement_rejects_positive_mr(self): self.assertIn('E_MR_TRIGGER',self.codes(bound_mutation('fixture://AWR007-M1',mr_state='PILOT'),True))
    def test_user_count_growth_can_be_typed_not_now(self): self.assertEqual([],validate_gate(bound_mutation('fixture://AWR007-M1',mr_state='NOT_NOW'),RULES,HERE))
    def test_multi_region_positive_requires_failover_owner(self):
        g=baseline(); g['multi_region']['failover_owner']=''; self.assertIn('E_MR_OWNER',self.codes(g))
    def test_multi_region_positive_requires_recovery_objective(self):
        g=baseline(); g['multi_region']['recovery_objective']={'rto_minutes':None,'rpo_minutes':None}; self.assertIn('E_MR_RECOVERY',self.codes(g))
    def test_scoped_residency_rejects_global_active_active_all_data(self): self.assertIn('E_MR_RESIDENCY_SCOPE',self.codes(bound_mutation('fixture://AWR007-M2',mr_state='PILOT'),True))
    def test_scoped_residency_can_be_review_required(self): self.assertEqual([],validate_gate(bound_mutation('fixture://AWR007-M2',mr_state='REVIEW_REQUIRED'),RULES,HERE))
    def test_unstable_conversion_signal_rejects_positive_ad(self): self.assertIn('E_AD_SIGNAL',self.codes(bound_mutation('fixture://AWR007-M3'),True))
    def test_unstable_signal_can_be_hold(self): self.assertEqual([],validate_gate(bound_mutation('fixture://AWR007-M3',ad_state='HOLD'),RULES,HERE))
    def test_ad_positive_requires_budget(self):
        g=baseline(); g['ad_automation']['budget_limit']=None; self.assertIn('E_AD_BUDGET',self.codes(g))
    def test_ad_positive_requires_targeting_scope(self):
        g=baseline(); g['ad_automation']['targeting_scope']=[]; self.assertIn('E_AD_TARGETING',self.codes(g))
    def test_ad_positive_requires_approval_owner_when_required(self):
        g=baseline(); g['ad_automation']['approval_state']='MISSING'; self.assertIn('E_AD_APPROVAL',self.codes(g))
    def test_ad_positive_requires_stop_loss_and_reconciliation(self):
        g=baseline(); g['ad_automation']['stop_loss']={}; self.assertIn('E_AD_GUARDRAIL',self.codes(g))
    def test_bounded_bid_only_mutation_can_remain_pilot(self): self.assertEqual([],validate_gate(bound_mutation('fixture://AWR007-M4'),RULES,HERE))
    def test_unrestricted_autonomy_rejected(self):
        g=baseline(); g['ad_automation']['automation_permission']='UNRESTRICTED_AUTONOMOUS_MUTATION'; self.assertIn('E_AD_UNBOUNDED',self.codes(g))
    def test_missing_decision_reported_without_answer_key(self):
        g=baseline(); del g['ad_automation']['decision']['state']; codes=self.codes(g); self.assertIn('E_AD_DECISION',codes); self.assertFalse(any(c.startswith('EXPECTED_') for c in codes))
    def test_decision_requires_stop_or_pivot(self):
        g=baseline(); g['multi_region']['decision']['stop_or_pivot_condition']=''; self.assertIn('E_MR_DECISION_META',self.codes(g))
    def test_decision_requires_governance_trigger(self):
        g=baseline(); g['ad_automation']['decision']['governance_triggers']=[]; self.assertIn('E_AD_DECISION_META',self.codes(g))
    def test_partial_is_allowed_non_success_state(self):
        g=baseline(); g['multi_region']['decision']=decision('PARTIAL'); self.assertEqual([],validate_gate(g,RULES,HERE))
    def test_disputed_is_allowed_non_success_state(self):
        g=baseline(); g['multi_region']['decision']=decision('DISPUTED'); self.assertEqual([],validate_gate(g,RULES,HERE))
    def test_visible_rules_are_consumed(self):
        bad=copy.deepcopy(RULES); bad['rules']=[]; self.assertIn('E_RULESET',{e['code'] for e in validate_gate(baseline(),bad,None)})

    def test_bound_missing_decisive_evidence_rejects_positive_multi_region(self):
        g=gate_from(HERE/'fixtures/missing_decisive_evidence_fixture.json','fixture://AWR007-MISSING-EVIDENCE-01'); g['multi_region']['decision']=decision('PILOT'); g['ad_automation']['decision']=decision('PILOT')
        codes=self.codes(g,True); self.assertIn('E_MR_OWNER',codes); self.assertNotIn('E_FIXTURE_REF',codes); self.assertNotIn('E_FIXTURE_BINDING',codes)

    def test_bound_missing_decisive_evidence_accepts_typed_hold(self):
        g=gate_from(HERE/'fixtures/missing_decisive_evidence_fixture.json','fixture://AWR007-MISSING-EVIDENCE-01'); g['multi_region']['decision']=decision('HOLD'); g['ad_automation']['decision']=decision('PILOT')
        self.assertEqual([],validate_gate(g,RULES,HERE))

    def test_ad_use_requires_completed_progression_evidence(self):
        g=baseline(); g['multi_region']['decision']=decision('NOT_NOW'); g['ad_automation']['decision']=decision('USE')
        self.assertIn('E_AD_USE_PROGRESSION',self.codes(g,True))
    def test_transfer_ad_use_can_pass_with_completed_progression_evidence(self):
        g=gate_from(HERE/'transfer/transfer_fixture.json','fixture://AWR007-TRANSFER-01'); g['multi_region']['decision']=decision('PILOT'); g['ad_automation']['decision']=decision('USE')
        self.assertEqual([],validate_gate(g,RULES,HERE))
    def test_ad_positive_requires_operational_ownership_cost(self):
        g=baseline(); g['ad_automation']['operational_ownership_cost']=''; self.assertIn('E_AD_OPERATIONAL_COST',self.codes(g))
    def test_multi_region_positive_requires_operational_ownership_cost(self):
        g=gate_from(HERE/'transfer/transfer_fixture.json','fixture://AWR007-TRANSFER-01'); g['multi_region']['operational_ownership_cost']=''; self.assertIn('E_MR_OPERATIONAL_COST',self.codes(g))

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