import copy, hashlib, json, pathlib, subprocess, unittest
HERE=pathlib.Path(__file__).resolve().parents[1]
SCRIPT=HERE/'adapter/ym-sync-v2-bridge-review.php'
VESSEL='17b19e77-0746-4cd6-9e37-31b2564ebddb'

def fixture(name): return json.loads((HERE/'fixtures'/name).read_text())
def env(payload, seq, type_, key, rev=1, vessel=VESSEL):
    body=json.dumps(payload,ensure_ascii=False,separators=(',',':'))
    return {'vessel_id':vessel,'seq_no':seq,'entity_type':type_,'entity_key':key,'entity_revision':rev,'content_hash':hashlib.sha256(body.encode()).hexdigest(),'payload_json':body}
def invoke(data,vessel=VESSEL):
    return subprocess.run(['php',str(SCRIPT),'--authenticated-vessel',vessel],input=json.dumps(data),text=True,capture_output=True)
class TestWireBridge(unittest.TestCase):
    def setUp(self):
        self.header=fixture('header.json')
        self.chunk=fixture('chunk-000.json')
        self.finalize=fixture('finalize.json')
        self.sid=self.header['header']['snapshot_id']
    def payload(self,kind):
        if kind=='header':return env(self.header,1,'signalk_catalog_manifest_v2',self.sid)
        if kind=='chunk':return env(self.chunk,2,'signalk_catalog_chunk_v2',self.sid+':0')
        return env(self.finalize,3,'signalk_catalog_finalize_v2',self.sid)
    def good(self,e):
        p=invoke(e)
        self.assertEqual(p.returncode,0,p.stderr)
        return json.loads(p.stdout)
    def bad(self,e,vessel=VESSEL):self.assertNotEqual(invoke(e,vessel).returncode,0)
    def test_header(self):self.assertEqual(self.good(self.payload('header'))['operation'],'header')
    def test_chunk(self):self.assertEqual(self.good(self.payload('chunk'))['operation'],'chunk')
    def test_finalize(self):self.assertEqual(self.good(self.payload('finalize'))['operation'],'finalize')
    def test_preserve_catalog_request(self):self.assertEqual(self.good(self.payload('chunk'))['catalog_request'],self.chunk)
    def test_authenticated_identity_mismatch(self):self.bad(self.payload('header'),'00000000-0000-4000-8000-000000000001')
    def test_envelope_vessel_mismatch(self):
        x=self.payload('header');x['vessel_id']='00000000-0000-4000-8000-000000000001';self.bad(x)
    def test_payload_vessel_mismatch(self):
        x=self.payload('header');p=json.loads(x['payload_json']);p['header']['vessel_id']='00000000-0000-4000-8000-000000000001'; x=env(p,1,x['entity_type'],self.sid);self.bad(x)
    def test_tampered_digest(self):
        x=self.payload('header');x['payload_json']+=' ';self.bad(x)
    def test_entity_type_mismatch(self):
        x=self.payload('header');x['entity_type']='signalk_catalog_chunk_v2';self.bad(x)
    def test_entity_key_mismatch(self):
        x=self.payload('chunk');x['entity_key']='fake';self.bad(x)
    def test_revision_mismatch(self):
        x=self.payload('chunk');x['entity_revision']=2;self.bad(x)
    def test_wrong_protocol(self):
        x=self.payload('header');p=json.loads(x['payload_json']);p['header']['protocol']='bad';x=env(p,1,x['entity_type'],self.sid);self.bad(x)
    def test_non_v2_rejected(self):
        x=self.payload('header');x['entity_type']='measurement_aggregate';self.bad(x)
    def test_extra_envelope_fields(self):
        x=self.payload('header');x['unknown']='x';self.bad(x)
    def test_invalid_seq(self):
        x=self.payload('header');x['seq_no']=0;self.bad(x)
    def test_invalid_chunk_index(self):
        x=self.payload('chunk');p=json.loads(x['payload_json']);p['chunk']['chunk_index']=-1;x=env(p,2,x['entity_type'],self.sid+':-1');self.bad(x)
    def test_exceeds_maximum(self):
        x=self.payload('header');x['payload_json']='a'*32000;x['content_hash']=hashlib.sha256(x['payload_json'].encode()).hexdigest();self.bad(x)
    def test_finalize_revision_not_provable(self):
        # Contract limitation recorded: finalize wire format has no revision.
        # The bridge accepts envelope revision; DB must independently bind snapshot to revision.
        x=self.payload('finalize');x['entity_revision']=999;self.assertEqual(self.good(x)['entity_revision'],999)
if __name__=='__main__': unittest.main()
