import copy
import datetime as dt
import json
import sys
import unittest

from cryptography.hazmat.primitives.asymmetric import ec
from odexa_ref import crypto, contracts as old
from profiles import free_contracts as f
from test_delegation_profile import fixture, NOW


def uid(number): return f'00000000-0000-4000-8000-{number:012d}'
def later(seconds): return (f.timestamp(NOW, 'now') + dt.timedelta(seconds=seconds)).strftime('%Y-%m-%dT%H:%M:%SZ')


def fixtures():
    authority, policy, operation = fixture()
    b = dict(protocol_version=f.VERSION, origin=policy['origin'], service_id=operation['service_id'],
             issuer=operation['issuer'], delegation_id=operation['delegation_id'])
    terms = b'Synthetic free terms. Retain attribution.'
    ar, pr = f.json_bytes(authority), f.json_bytes(policy)
    request = dict(b, principal_id='https://agent.example/principal', request=dict(url=b['origin']+'/about',
        actions=['retrieve','store'], purposes=['public_retrieval'], supported_obligations=['attribution']),
        access_seconds=300, use_seconds=600)
    offer = dict(request, offer_id=uid(1), client_id='agent-one', policy_id=policy['policy_id'], policy_revision=1,
        policy_digest=f.digest(pr), authority_revision=1, authority_digest=f.digest(ar), terms_digest=f.digest(terms),
        obligations=[dict(type='attribution',name='Example originator',url=b['origin']+'/about')],
        context=[dict(role=role,url=url,digest=f.digest(raw),media_type=media) for role,url,raw,media in [
            ('policy',b['origin']+'/contexts/policy',pr,'application/json'),
            ('human_terms','https://provider.example/tenant-a/contexts/terms',terms,'text/plain'),
            ('origin_authority',b['origin']+'/odexa-service/history/1.json',ar,'application/json')]],
        nonce=crypto.b64u(b'n'*32),issued_at=NOW,expires_at=later(60),revocation='future_access_only',payment={'required':False})
    offer_bytes=f.json_bytes(offer)
    acceptance=dict(b,offer_id=offer['offer_id'],offer_digest=f.digest(offer_bytes),client_id='agent-one',
        principal_id=request['principal_id'],intent='accept',nonce=offer['nonce'],accepted_at=NOW,idempotency_key='idempotency-00001')
    acceptance_bytes=f.json_bytes(acceptance)
    receipt=dict(b,record_type='agreement.accepted',agreement_id=uid(2),client_id='agent-one',principal_id=request['principal_id'],
        offer_id=offer['offer_id'],offer_digest=f.digest(offer_bytes),acceptance_digest=f.digest(acceptance_bytes),recorded_at=NOW,
        access_expires_at=later(300),use_expires_at=later(600),state_at_issue='active',payment_required=False,access_credential=False)
    status=dict(b,record_type='agreement.status',agreement_id=uid(2),state='active',effective_at=NOW,reason='accepted',
        status_version=1,issued_at=NOW,authority_revision=1,authority_digest=f.digest(ar),authority_checked_at=NOW)
    revoke=dict(b,agreement_id=uid(2),reason='operator_request',idempotency_key='revoke-key-000001')
    token_request=dict(b,agreement_id=uid(2))
    token_response=dict(b,agreement_id=uid(2),token_type='Bearer',access_token=crypto.b64u(b't'*32),expires_at=later(120))
    introspection_request=dict(b,token=token_response['access_token'],resource_url=b['origin']+'/about',method='GET',
        actions=['retrieve'],purposes=['public_retrieval'],request_id=uid(3))
    admission=dict(decision_id=uid(4),agreement_id=uid(2),client_id='agent-one',principal_id=request['principal_id'],
        resource_url=b['origin']+'/about',method='GET',actions=['retrieve'],purposes=['public_retrieval'],expires_at=later(120),
        checked_at=NOW,status_version=1,authority_revision=1,authority_digest=f.digest(ar),authority_checked_at=NOW,
        policy_id=policy['policy_id'],policy_revision=1,policy_digest=f.digest(pr))
    introspection_response=dict(b,request_id=uid(3),active=True,permitted=True,reason=None,admission=admission)
    report=dict(b,event_id=uid(5),event_type='use.reported',occurred_at=later(1),reporter_id='https://agent.example/reporter',
        source='client_reported',resource_url=b['origin']+'/about',policy_id=policy['policy_id'],policy_revision=1,
        actions=['store'],purposes=['public_retrieval'],agreement_id=uid(2),asset_ref=None,
        operation=dict(id=uid(6),started_at=NOW,ended_at=later(1),state='completed'),http=None,quantity=1,unit='operation',related_events=[],derived_from=[])
    raw=f.json_bytes(report)
    signed=crypto.sign_jws(raw,ec.derive_private_key(3,ec.SECP256R1()),'https://agent.example/keys/one','odexa-event+jws')
    intake=dict(b,record_id=uid(7),collector_id=b['service_id'],received_at=later(2),assurance='client_key_verified',
        authenticated_reporter_id=report['reporter_id'],payload_digest=f.digest(raw),payload_b64url=crypto.b64u(raw),reporter_jws=signed,
        authority=dict(origin=b['origin'],service_id=b['service_id'],revision=1,url=b['origin']+'/odexa-service/history/1.json',digest=f.digest(ar)))
    return dict(binding=b,offer_request=request,offer=offer,acceptance=acceptance,receipt=receipt,status=status,
        revoke_request=revoke,token_request=token_request,token_response=token_response,introspection_request=introspection_request,
        introspection_response=introspection_response,report=report,intake=intake), (pr,ar,terms)


def schema_cases():
    documents,_=fixtures()
    cases=[dict(name='valid_'+name,definition=name,value=copy.deepcopy(value),structural_valid=True,semantic_valid=True)
           for name,value in documents.items()]
    def bad(name,definition,change,structural=False,semantic=False):
        value=copy.deepcopy(documents[definition]); change(value)
        cases.append(dict(name=name,definition=definition,value=value,structural_valid=structural,semantic_valid=semantic))
    bad('wrong_version','offer',lambda v:v.update(protocol_version='1.2.0-draft.2'))
    bad('missing_issuer','offer_request',lambda v:v.pop('issuer'))
    bad('unknown_acceptance_member','acceptance',lambda v:v.update(extra='x'))
    bad('paid_offer','offer',lambda v:v.update(payment={'required':True}))
    bad('invalid_receipt_uuid','receipt',lambda v:v.update(agreement_id='not-uuid'))
    bad('denied_with_admission','introspection_response',lambda v:v.update(permitted=False,reason='scope_mismatch'))
    bad('permission_without_active_token','introspection_response',lambda v:v.update(active=False))
    bad('client_reports_origin_source','report',lambda v:v.update(source='origin_observed'))
    bad('receipt_is_not_bearer','receipt',lambda v:v.update(access_credential=True))
    bad('decimal_duration','offer_request',lambda v:v.update(access_seconds=1.0),True)
    bad('unsafe_request_url','offer_request',lambda v:v['request'].update(url=v['origin']+'/a%2Fb'),True)
    bad('access_exceeds_use','offer_request',lambda v:v.update(access_seconds=601),True)
    bad('offer_bad_context_digest','offer',lambda v:v['context'][0].update(digest='sha256:'+'0'*64),True)
    bad('offer_duplicate_context_role','offer',lambda v:v['context'][1].update(role='policy'),True)
    bad('receipt_reverse_window','receipt',lambda v:v.update(access_expires_at=NOW),True)
    bad('stale_status_authority','status',lambda v:v.update(issued_at=later(6)),True)
    bad('bad_calendar','token_response',lambda v:v.update(expires_at='2026-02-30T00:00:00Z'),True)
    bad('wrong_intake_payload_digest','intake',lambda v:v.update(payload_digest='sha256:'+'0'*64),True)
    return dict(schema='free',cases=cases)


class FreeContractsTests(unittest.TestCase):
    def setUp(self): self.values,self.context=fixtures()
    def bad(self, function, *args, **kwargs):
        with self.assertRaises((ValueError,TypeError)): function(*args,**kwargs)
    def test_every_positive_shape_and_context_chain(self):
        for name,value in self.values.items():
            with self.subTest(name=name):getattr(f,'validate_'+name)(value)
        offer=self.values['offer']; acceptance=self.values['acceptance']; receipt=self.values['receipt']
        ob,ab=f.json_bytes(offer),f.json_bytes(acceptance)
        f.validate_offer(offer,self.values['offer_request'])
        checked=f.validate_context_bundle(offer,policy_bytes=self.context[0],authority_bytes=self.context[1],terms_bytes=self.context[2],now=NOW)
        self.assertFalse(checked['origin_authenticated'])
        f.validate_acceptance(acceptance,ob,NOW);f.validate_receipt(receipt,ob,ab)
        f.validate_status(self.values['status'],receipt);f.validate_token_response(self.values['token_response'],receipt,NOW)
        f.validate_introspection_response(self.values['introspection_response'],self.values['introspection_request'],receipt,self.values['token_response'])
    def test_no_legacy_envelope_or_global_version_mutation(self):
        before=old.VERSION; types=set(crypto.TYPES)
        for name,value in self.values.items():
            v=copy.deepcopy(value);v['protocol_version']='1.2.0-draft.2';self.bad(getattr(f,'validate_'+name),v)
        self.assertEqual(old.VERSION,before);self.assertEqual(crypto.TYPES,types)
    def test_closed_keys_and_strict_wire_numbers(self):
        for name,value in self.values.items():
            v=copy.deepcopy(value);v['unexpected']=1;self.bad(getattr(f,'validate_'+name),v)
        raw=f.json_bytes(self.values['offer_request'])
        for bad in [b'{"x":1,"x":2}',b'\xff',raw.replace(b'"access_seconds":300',b'"access_seconds":300.0'),b'{"x":-0}']:
            self.bad(f.load_json,bad)
    def test_noncanonical_nonce_and_paid_profile_rejected(self):
        for bad in ['n'*43,crypto.b64u(b'n'*31),crypto.b64u(b'n'*32)+'=']:
            v=copy.deepcopy(self.values['offer']);v['nonce']=bad;self.bad(f.validate_offer,v)
        for bad in [True,0,'false']:
            v=copy.deepcopy(self.values['offer']);v['payment']['required']=bad;self.bad(f.validate_offer,v)
    def test_context_exact_bytes_and_semantics(self):
        offer=self.values['offer'];pr,ar,terms=self.context
        for args in [(pr+b' ',ar,terms),(pr,ar+b' ',terms),(pr,ar,terms+b' '),(pr,ar,b'\xff')]:
            self.bad(f.validate_context_bundle,offer,policy_bytes=args[0],authority_bytes=args[1],terms_bytes=args[2])
        v=copy.deepcopy(offer);v['obligations']=[]
        self.bad(f.validate_context_bundle,v,policy_bytes=pr,authority_bytes=ar,terms_bytes=terms)
        v=copy.deepcopy(offer);v['context'][1]['url']='https://unappointed.example/terms'
        self.bad(f.validate_context_bundle,v,policy_bytes=pr,authority_bytes=ar,terms_bytes=terms)
    def test_assent_binds_exact_offer_principal_nonce_and_window(self):
        ob=f.json_bytes(self.values['offer'])
        for field,bad in [('origin','https://other.example'),('client_id','other'),('principal_id','https://other.example/person'),('nonce',crypto.b64u(b'x'*32)),('accepted_at',later(60)),('offer_digest','sha256:'+'0'*64)]:
            v=dict(self.values['acceptance']);v[field]=bad;self.bad(f.validate_acceptance,v,ob,later(61))
        self.bad(f.validate_acceptance,self.values['acceptance'],ob+b' ',NOW)
    def test_receipt_binds_decoded_acceptance_bytes_and_exact_windows(self):
        ob=f.json_bytes(self.values['offer']);ab=f.json_bytes(self.values['acceptance'])
        self.bad(f.validate_receipt,self.values['receipt'],ob,ab+b' ')
        for field,bad in [('access_expires_at',later(301)),('use_expires_at',later(601)),('recorded_at',later(60)),('access_credential',True),('payment_required',0)]:
            v=dict(self.values['receipt']);v[field]=bad;self.bad(f.validate_receipt,v,ob,ab)
    def test_resigned_same_acceptance_payload_has_same_receipt_binding(self):
        raw=f.json_bytes(self.values['acceptance']);key=ec.derive_private_key(4,ec.SECP256R1())
        first=crypto.sign_jws(raw,key,'https://agent.example/key','odexa-acceptance+jws')
        second=crypto.sign_jws(raw,key,'https://agent.example/key','odexa-acceptance+jws')
        self.assertEqual(crypto.split_jws(first)[3],crypto.split_jws(second)[3])
        f.validate_receipt(self.values['receipt'],f.json_bytes(self.values['offer']),crypto.split_jws(second)[3])
    def test_status_is_bound_current_observation_without_access_extension(self):
        receipt=self.values['receipt']
        for change in [dict(agreement_id=uid(99)),dict(issued_at=later(6)),dict(state='expired',effective_at=NOW),dict(status_version=0)]:
            v=dict(self.values['status'],**change);self.bad(f.validate_status,v,receipt)
        f.validate_revoke_request(self.values['revoke_request'],uid(2))
        self.bad(f.validate_revoke_request,self.values['revoke_request'],uid(99))
    def test_token_expiry_bound_to_clock_and_agreement(self):
        v=dict(self.values['token_response'],expires_at=later(301));self.bad(f.validate_token_response,v,self.values['receipt'],NOW)
        v=dict(self.values['token_response'],expires_at=NOW);self.bad(f.validate_token_response,v,self.values['receipt'],NOW)
        v=dict(self.values['token_response'],agreement_id=uid(99));self.bad(f.validate_token_response,v,self.values['receipt'],NOW)
    def test_introspection_allows_no_token_echo_and_requires_both_booleans(self):
        v=copy.deepcopy(self.values['introspection_response']);v['access_token']='never-echo';self.bad(f.validate_introspection_response,v)
        for change in [dict(active=False),dict(permitted=1),dict(permitted=False,reason='scope_mismatch')]:
            v=dict(self.values['introspection_response'],**change);self.bad(f.validate_introspection_response,v)
        b=self.values['binding']
        for active,reason in [(False,'invalid_token'),(False,'inactive_agreement'),(True,'scope_mismatch')]:
            f.validate_introspection_response(dict(b,request_id=uid(3),active=active,permitted=False,reason=reason,admission=None))
    def test_introspection_request_principal_scope_and_expiry_correlation(self):
        for field,bad in [('principal_id','https://agent.example/other'),('client_id','other'),('method','HEAD'),('resource_url',self.values['binding']['origin']+'/other'),('expires_at',later(121))]:
            v=copy.deepcopy(self.values['introspection_response']);v['admission'][field]=bad
            self.bad(f.validate_introspection_response,v,self.values['introspection_request'],self.values['receipt'],self.values['token_response'])
    def test_report_roles_and_intake_cannot_promote_claims(self):
        f.validate_report(self.values['report'],role='agent',reporter_id=self.values['report']['reporter_id'])
        self.bad(f.validate_report,self.values['report'],role='gateway')
        for change in [dict(assurance='origin_key_verified'),dict(authenticated_reporter_id='https://other.example/reporter'),dict(payload_digest='sha256:'+'0'*64)]:
            self.bad(f.validate_intake,dict(self.values['intake'],**change))
        unsigned=dict(self.values['intake'],assurance='unverified',authenticated_reporter_id=None,reporter_jws=None)
        f.validate_intake(unsigned)
    def test_delivery_report_matches_admission_and_preserves_http_semantics(self):
        value=copy.deepcopy(self.values['report']);a=self.values['introspection_response']['admission'];origin=value['origin']
        value.update(source='origin_observed',event_type='delivery.completed',reporter_id=origin+'/gateway',actions=a['actions'],operation=None)
        value['http']=dict(method='GET',status=200,kind='full',delivery_id=uid(3),hop_id=uid(8),ingress_id=origin+'/edge',hop_role='end_client',boundary_id=origin+'/end-client',cache_status='bypass',content_codings=[],content_bytes=3,content_digest=f.digest(b'abc'),decoded_bytes=3,decoded_digest=f.digest(b'abc'),range=None,representation_metadata=None)
        f.validate_report(value,role='gateway',admission=self.values['introspection_response'])
        for field,bad in [('delivery_id',uid(99)),('method','HEAD')]:
            v=copy.deepcopy(value);v['http'][field]=bad;self.bad(f.validate_report,v,admission=self.values['introspection_response'])
        v=copy.deepcopy(value);v['actions']=['retrieve','store'];self.bad(f.validate_report,v,admission=self.values['introspection_response'])
    def test_schema_case_semantics_match(self):
        for case in schema_cases()['cases']:
            with self.subTest(name=case['name']):
                function=getattr(f,'validate_'+case['definition'])
                if case['semantic_valid']:function(case['value'])
                else:self.bad(function,case['value'])


if __name__=='__main__':
    if '--cases' in sys.argv:print(json.dumps(schema_cases(),sort_keys=True))
    else:unittest.main()
