import json,pathlib,copy,sys
from evaluator import evaluate
root=pathlib.Path(__file__).resolve().parent
fx=json.loads((root/'fixtures/cases.json').read_text())
base=fx['baseline']

def m(**kw):
    x=copy.deepcopy(base); x.update(kw); return x

def recalc(x):
    unc=x['input_tokens']-x['cached_input_tokens']
    x['measured_cost']=unc*x['input_unit_price']+x['cached_input_tokens']*x['cached_input_unit_price']+x['output_tokens']*x['output_unit_price']+x['tool_usage_units']*x['tool_unit_price']+x['external_provider_cost']+x['retry_cost']
    x['allocated_shared_cost']=x['shared_cost_pool']*(x['allocation_weight']/x['allocation_basis_total']) if x['allocation_basis_total'] else 0
    x['allocated_cost']=x['measured_cost']+x['allocated_shared_cost']
    x['unit_cost']=x['allocated_cost']/x['unit_count'] if x['unit_count'] else 0
    x['budget_consumed']=x['budget_consumed_before']+x['allocated_cost']
    x['budget_state']='STOP' if x['budget_consumed']>=x['stop_threshold'] else ('WARNING' if x['budget_consumed']>=x['warning_threshold'] else 'WITHIN')
    x['escalation_required']='YES' if x['budget_state'] in {'WARNING','STOP'} else 'NO'
    return x

tests=[]
def add(name,x,expected): tests.append((name,x,expected))
add('baseline',copy.deepcopy(base),'COST_LEDGER_REVIEW_READY')
# Required/enum/state gates
add('missing_run_id',m(run_id='MISSING'),'HOLD_REQUIRED_EVIDENCE_MISSING:run_id')
add('missing_audit',m(audit_ref='MISSING'),'HOLD_REQUIRED_EVIDENCE_MISSING:audit_ref')
add('invalid_cost_state',m(cost_state='MAGIC'),'HOLD_COST_STATE_INVALID')
add('invalid_missing_state',m(missing_usage_state='MAYBE'),'HOLD_MISSING_USAGE_STATE_INVALID')
add('invalid_price_state',m(price_staleness_state='FRESHISH'),'HOLD_PRICE_STALENESS_STATE_INVALID')
add('invalid_budget_state',m(budget_state='OK'),'HOLD_BUDGET_STATE_INVALID')
add('invalid_usage_source',m(usage_source='DASHBOARD_GUESS'),'HOLD_USAGE_SOURCE_INVALID')
# Usage integrity
add('token_total_mismatch',m(total_tokens=149999),'HOLD_TOKEN_TOTAL_MISMATCH')
add('cached_gt_input',m(cached_input_tokens=130000),'HOLD_CACHED_INPUT_GT_INPUT')
add('reasoning_gt_output',m(reasoning_tokens=40000),'HOLD_REASONING_GT_OUTPUT')
add('requests_gt_expected',m(requests_count=4,expected_requests_count=3),'HOLD_REQUEST_COUNT_INVALID')
add('request_missing_but_complete',m(requests_count=2,expected_requests_count=3),'HOLD_USAGE_COUNT_CONTRADICTION')
add('partial_adapter_marked_complete',m(usage_detail_state='PARTIAL_ADAPTER'),'HOLD_PARTIAL_ADAPTER_MARKED_COMPLETE')
add('critical_usage_missing',m(missing_usage_state='MISSING_CRITICAL',cost_state='PARTIAL'),'HOLD_CRITICAL_USAGE_MISSING')
add('partial_usage_exact',m(requests_count=2,missing_usage_state='MISSING_PARTIAL',usage_detail_state='PARTIAL_ADAPTER'),'HOLD_MISSING_USAGE_CANNOT_BE_EXACT')
# price binding
add('model_price_mismatch',m(model_id='model-synth-b'),'HOLD_PRICE_MODEL_MISMATCH')
add('provider_price_mismatch',m(provider_id='provider-other'),'HOLD_PRICE_PROVIDER_MISMATCH')
add('tier_price_mismatch',m(processing_tier='priority'),'HOLD_PRICE_TIER_MISMATCH')
add('currency_mismatch',m(currency='EUR'),'HOLD_PRICE_CURRENCY_MISMATCH')
add('price_after_run',m(price_effective_at='2026-09-15T00:00:00Z'),'HOLD_PRICE_EFFECTIVE_AFTER_RUN')
add('stale_price_exact',m(price_staleness_state='STALE'),'HOLD_STALE_PRICE_CANNOT_BE_EXACT')
add('unknown_price_exact',m(price_staleness_state='UNKNOWN'),'HOLD_STALE_PRICE_CANNOT_BE_EXACT')
# monetary recomputation
add('direct_cost_mismatch',m(measured_cost=0.700),'HOLD_DIRECT_COST_MISMATCH')
x=m(retry_cost=0.08); add('retry_charge_changed_cost_not_recomputed',x,'HOLD_DIRECT_COST_MISMATCH')
x=m(external_provider_cost=0.20); add('external_charge_changed_cost_not_recomputed',x,'HOLD_DIRECT_COST_MISMATCH')
x=m(tool_usage_units=3); add('tool_units_changed_cost_not_recomputed',x,'HOLD_DIRECT_COST_MISMATCH')
# allocation
add('allocation_rule_missing',m(allocation_rule_ref='NONE'),'HOLD_ALLOCATION_RULE_MISSING_OR_SILENT')
add('silent_equal_split',m(allocation_rule_ref='SILENT_EQUAL_SPLIT'),'HOLD_ALLOCATION_RULE_MISSING_OR_SILENT')
add('allocation_basis_zero',m(allocation_basis_total=0),'HOLD_ALLOCATION_BASIS_INVALID')
add('shared_allocation_mismatch',m(allocated_shared_cost=0.10),'HOLD_SHARED_ALLOCATION_MISMATCH')
add('allocated_cost_mismatch',m(allocated_cost=0.90),'HOLD_ALLOCATED_COST_MISMATCH')
add('unit_count_zero',m(unit_count=0),'HOLD_UNIT_COUNT_INVALID')
add('unit_cost_mismatch',m(unit_cost=0.50),'HOLD_UNIT_COST_MISMATCH')
# budgets
add('budget_consumption_mismatch',m(budget_consumed=3.9),'HOLD_BUDGET_CONSUMPTION_MISMATCH')
add('warning_gt_stop',m(warning_threshold=5.5,stop_threshold=5.0),'HOLD_BUDGET_THRESHOLD_ORDER_INVALID')
add('stop_gt_limit',m(stop_threshold=5.5,budget_limit=5.0),'HOLD_BUDGET_THRESHOLD_ORDER_INVALID')
add('budget_state_wrong',m(budget_state='WARNING'),'HOLD_BUDGET_STATE_MISMATCH')
x=m(budget_consumed_before=3.5); recalc(x); x['escalation_required']='NO'; add('warning_requires_escalation',x,'HOLD_BUDGET_ESCALATION_REQUIRED')
x=m(budget_consumed_before=4.3); recalc(x); x['escalation_required']='NO'; add('stop_requires_escalation',x,'HOLD_BUDGET_ESCALATION_REQUIRED')
x=m(budget_consumed_before=3.5); recalc(x); add('valid_warning',x,'WARNING_BUDGET_REVIEW_REQUIRED')
x=m(budget_consumed_before=4.3); recalc(x); add('valid_stop',x,'STOP_BUDGET_ESCALATION_REQUIRED')
# exactness and aux validation
x=m(cost_state='ESTIMATE'); add('complete_current_should_be_exact',x,'HOLD_COMPLETE_CURRENT_COST_NOT_EXACT')
add('negative_token',m(input_tokens=-1),'HOLD_NEGATIVE_OR_NONNUMERIC:input_tokens')
add('negative_price',m(input_unit_price=-0.1),'HOLD_NEGATIVE_OR_NONNUMERIC:input_unit_price')
add('missing_owner',m(owner_id='MISSING'),'HOLD_AUX_EVIDENCE_MISSING:owner_id')
add('missing_price_snapshot',m(price_snapshot_id='MISSING'),'HOLD_REQUIRED_EVIDENCE_MISSING:price_snapshot_id')
# Transfer gates
tr=copy.deepcopy(fx['transfer']); add('transfer_partial_valid',tr,'TRANSFER_COST_REVIEW_REQUIRED')
x=copy.deepcopy(tr); x['transfer_recomputed']=False; add('transfer_not_recomputed',x,'HOLD_TRANSFER_NOT_RECOMPUTED')
x=copy.deepcopy(tr); x['baseline_exact_cost_reused']=True; add('transfer_baseline_exact_reuse',x,'HOLD_TRANSFER_BASELINE_EXACT_REUSE')
x=copy.deepcopy(tr); x['cost_state']='EXACT'; add('transfer_partial_marked_exact',x,'HOLD_MISSING_USAGE_CANNOT_BE_EXACT')
x=copy.deepcopy(tr); x['allocation_rule_ref']='SILENT_EQUAL_SPLIT'; add('transfer_silent_split',x,'HOLD_ALLOCATION_RULE_MISSING_OR_SILENT')
# Transfer with complete usage should be review-ready when recomputed.
x=copy.deepcopy(base); x.update({'run_id':'run-t-complete','request_id':'req-t','transfer_variant':'PRICE_CHANGE_COMPLETE','transfer_recomputed':True,'baseline_exact_cost_reused':False,'audit_ref':'audit://agn007/transfer-complete'}); add('transfer_complete_valid',x,'TRANSFER_COST_REVIEW_READY')
# Changed price with old exact monetary calculation.
x=copy.deepcopy(base); x['input_unit_price']=0.0000022; add('price_changed_cost_not_recomputed',x,'HOLD_DIRECT_COST_MISMATCH')
# Shared pool changed but old allocation retained.
x=copy.deepcopy(base); x['shared_cost_pool']=0.4; add('shared_pool_changed_old_allocation',x,'HOLD_SHARED_ALLOCATION_MISMATCH')
# Explicit equal split can be valid when represented as explicit weights.
x=copy.deepcopy(base); x.update({'allocation_rule_ref':'EXPLICIT_EQUAL_SPLIT','allocation_weight':50,'allocation_basis_total':100,'allocated_shared_cost':0.10}); recalc(x); add('explicit_equal_split_valid',x,'COST_LEDGER_REVIEW_READY')

fails=[]
for name,x,expected in tests:
    got=evaluate(x)
    if got!=expected: fails.append((name,expected,got))
print(f"PASS {len(tests)}/{len(tests)}" if not fails else f"FAIL {len(tests)-len(fails)}/{len(tests)}")
for f in fails: print('  ',f)
sys.exit(1 if fails else 0)
