"""IMM1344 incremental writer contract and signed-packet preservation."""
import unittest, tempfile, json, sys, os, re
from pathlib import Path
from lxml import etree as E
import pikepdf
sys.path.insert(0,str(Path(__file__).resolve().parent))
from imm1344_pdf import fill_sponsorship
BASE=Path(__file__).resolve().parents[2]
WORK=BASE/'storage/app/imm/tmp/imm1344-work'
WORK.mkdir(parents=True,exist_ok=True)
def packets(p):
 with pikepdf.open(p) as pdf:
  x=pdf.Root.AcroForm.XFA
  return {str(x[i]):x[i+1].read_bytes() for i in range(0,len(x),2)}
class Imm1344WriterTest(unittest.TestCase):
 def test_signed_packets_and_phone_visibility(self):
  for lang,suffix in [('en','e'),('fr','f')]:
   c=json.loads(Path(__file__).with_name(f'imm1344-contract.{lang}.json').read_text(encoding='utf8'))
   root='form1.Part1.SponsorContactInfo.q3-4.Phone.'
   alt='form1.Part1.SponsorContactInfo.q3-4.AltPhone.'
   fields={root+'CanadaUS':'1',root+'Other':'0',root+'Type':'02',root+'NumberCountry':'1',root+'NumberExt':'101',root+'NANumber.AreaCode':'416',root+'NANumber.FirstThree':'555',root+'NANumber.LastFive':'0100',alt+'CanadaUS':'0',alt+'Other':'1',alt+'Type':'01',alt+'NumberCountry':'94',alt+'IntlNumber.IntlNumber':'771234567','form1.Part1.SponsorDetails.genDetails.q3.CoSignerInd':'N','form1.Part1.SponsorDetails.q1.FamilyName':'LEE'}
   blank=BASE/f'storage/app/imm/imm1344{suffix}.pdf'
   with tempfile.TemporaryDirectory(dir=BASE/'storage/app/imm/tmp/imm1344-work') as tmp:
    out=Path(tmp)/'draft.pdf';fill_sponsorship({'form':'IMM1344','language':lang,'fields':fields},out,blank)
    self.assertTrue(out.read_bytes().startswith(blank.read_bytes()))
    old,new=packets(blank),packets(out)
    # Only applicant data may change: Adobe LOV packet bytes stay signed-source
    # identical, including namespace prefixes, entities and processing instructions.
    prefix=lambda raw:raw[:re.search(br'<xfa:data(?:\s|>)',raw).start()]
    self.assertEqual(prefix(old['datasets']),prefix(new['datasets']))
    for key in old:
     if key not in ['datasets','form']:self.assertEqual(old[key],new[key],key)
    d=E.fromstring(new['datasets']);f=E.fromstring(new['form'])
    self.assertEqual('416',d.xpath('//*[local-name()="Phone"]/*[local-name()="NANumber"]/*[local-name()="AreaCode"]')[0].text)
    self.assertFalse(f.xpath('//*[local-name()="exclGroup"]/*[local-name()="value"]'))
    self.assertEqual('visible',f.xpath('//*[@name="Phone"]/*[@name="NANumber"]')[0].get('presence'))
    self.assertEqual('hidden',f.xpath('//*[@name="CoSigner"]')[0].get('presence'))
    self.assertEqual('1',f.xpath('//*[@name="Phone"]/*[@name="CanadaUS"]/*[local-name()="value"]/*/text()')[0])
    self.assertEqual('readOnly',f.xpath('//*[@name="Phone"]/*[@name="NumberCountry"]')[0].get('access'))
    self.assertTrue(f.xpath('//*[@activity="docReady"]/*[local-name()="script" and contains(text(),"imm1344Restore")]'))
    original=E.fromstring(old['template']).xpath('//*[@name="form1"]/*[@activity="docReady"]/*[local-name()="script"]/text()')[0]
    saved=f.xpath('//*[@name="form1"]/*[@activity="docReady"]/*[local-name()="script"]/text()')[0]
    # Adobe restores saved access/presence deltas; PDFium merges them directly.
    # Never conditionally skip the template's manual state restoration.
    self.assertTrue(saved.startswith(original), 'Official saved-state restore must run unconditionally before hydration')
    early=f.xpath('//*[@name="form1"]/*[@activity="initialize"]/*[local-name()="script"]/text()')
    self.assertTrue(early, 'Phone state must be initialized before automatic validation, even without postal answers')
    self.assertLess(early[0].index('cc.access='),early[0].index('LOVUtils.initLOV()'))
    self.assertNotIn('String(c.rawValue)==="1"', saved)
    self.assertIn('String(cd.value)==="1"', saved)
    self.assertEqual('visible', f.xpath('//*[@name="AltPhone"]/*[@name="IntlNumber"]')[0].get('presence'))
    self.assertEqual('invisible', f.xpath('//*[@name="AltPhone"]/*[@name="NANumber"]')[0].get('presence'))
    self.assertEqual('101', f.xpath('//*[@name="Phone"]/*[@name="NumberExt"]/*[local-name()="value"]/*/text()')[0])
 def test_rejects_unknown_values_and_signature_targets(self):
  blank=BASE/'storage/app/imm/imm1344e.pdf'
  with tempfile.TemporaryDirectory(dir=BASE/'storage/app/imm/tmp/imm1344-work') as tmp:
   for fields in [{'form1.unknown':'x'},{'form1.Part2.Signatures.Sigs.SponSigDate':'2026-01-01'},{'form1.Part1.SponsorDetails.genDetails.q3.CoSignerInd':'yes'}]:
    with self.assertRaises(ValueError):fill_sponsorship({'form':'IMM1344','language':'en','fields':fields},Path(tmp)/'bad.pdf',blank)
 @unittest.skipUnless(os.environ.get('IMM1344_XFA_RUNTIME'),'Optional native XFA runtime not configured')
 def test_native_phone_postal_and_subsequent_manual_value_changes(self):
  sys.path.insert(0,os.environ['IMM1344_XFA_RUNTIME'])
  import pypdfium2_raw.bindings as raw
  raw.FPDF_ERR_XFALOAD=7;raw.FPDF_ERR_XFALAYOUT=8
  import pypdfium2 as pdfium
  from imm1344_pdf import resolve
  from imm0008_xfa_checksum import form_checksum
  from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
  from pyhanko.pdf_utils import generic
  phone='form1.Part1.SponsorContactInfo.q3-4.Phone.'
  address='form1.Part1.SponsorContactInfo.q1.AddressRow2.'
  fields={phone+'CanadaUS':'1',phone+'Other':'0',phone+'Type':'02',phone+'NumberCountry':'1',phone+'NANumber.AreaCode':'416',phone+'NANumber.FirstThree':'555',phone+'NANumber.LastFive':'0100',address+'Country':'511',address+'ProvinceState':'06',address+'PostalCode':'M5V 2T6','form1.Part1.SponsorDetails.genDetails.q3.CoSignerInd':'N'}
  co='form1.Part1.CoSigner.CosignContactInfo.q1-2.Phone.'
  fields.update({co+'CanadaUS':'1',co+'Other':'0',co+'Type':'03',co+'NumberCountry':'1',co+'NumberExt':'102',co+'NANumber.AreaCode':'416',co+'NANumber.FirstThree':'555',co+'NANumber.LastFive':'0103','form1.Part1.SponsorDetails.genDetails.q3.CoSignerInd':'Y'})
  with tempfile.TemporaryDirectory(dir=WORK) as tmp:
   tmp=Path(tmp)
   for lang,suffix in [('en','e'),('fr','f')]:
    out=tmp/f'{lang}.pdf';fill_sponsorship({'form':'IMM1344','language':lang,'fields':fields},out,BASE/f'storage/app/imm/imm1344{suffix}.pdf')
    for edited in [False,True]:
     expected=dict(fields);source=out
     if edited:
      # Simulate changed saved answers without regenerating the original hook.
      expected[phone+'NANumber.LastFive']='0199';expected[address+'PostalCode']='K1A 0B1'
      expected[co+'NANumber.LastFive']='0198'
      pk=packets(out);data=E.fromstring(pk['datasets']);form=E.fromstring(pk['form'])
      for path,value in expected.items():resolve(data,path).text=value
      datasets=E.tostring(data);form.set('checksum',form_checksum(pk['template'],datasets));source=tmp/f'{lang}-edited.pdf'
      with out.open('rb') as original:
       writer=IncrementalPdfFileWriter(original,strict=False)
       if writer.prev.encrypted:writer.encrypt('')
       x=writer.root['/AcroForm']['/XFA'];updates={'datasets':datasets,'form':E.tostring(form)}
       for i in range(0,len(x),2):
        if str(x[i]) in updates:
         ref=x.raw_get(i+1);stream=generic.StreamObject(stream_data=updates[str(x[i])]);stream.compress();writer.objects[(ref.generation,ref.idnum)]=stream
       with source.open('wb') as target:writer.write(target)
     doc=pdfium.PdfDocument(source);doc.init_forms();saved=tmp/f'{lang}-native-{edited}.pdf';doc.save(saved);doc.close()
     result=packets(saved);data=E.fromstring(result['datasets'])
     for path,value in expected.items():self.assertEqual(value,(resolve(data,path).text or '').strip(),path)
     merged=E.fromstring(result['form'])
     self.assertEqual('visible',merged.xpath('//*[@name="Phone"]/*[@name="NANumber"]')[0].get('presence'))
     contact=merged.xpath('//*[@name="CosignContactInfo"]')[0]
     self.assertEqual('readOnly',contact.xpath('.//*[@name="Phone"]/*[@name="NumberCountry"]')[0].get('access'))
     self.assertEqual('1',contact.xpath('.//*[@name="Phone"]/*[@name="CanadaUS"]/*[local-name()="value"]/*/text()')[0].strip())
if __name__=='__main__':unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout))
