"""IMM5406 isolated contracts, dynamic rows and Adobe-compatible saved XML."""
import hashlib
import json
import os
from pathlib import Path
import sys
import tempfile
import unittest
import pikepdf
from lxml import etree as E

BASE = Path(__file__).resolve().parents[2]


class Imm5406Test(unittest.TestCase):
    def test_partner_branch_is_replayed_after_host_merge_without_template_changes(self):
        from imm5406_pdf import fill_family_information
        for lang,suffix in [('en','e'),('fr','f')]:
            contract=json.loads((BASE/f'resources/imm/imm5406-contract.{lang}.json').read_text(encoding='utf8'))
            path=contract['groups']['applicant']['fields']['marital_status']['path']
            blank=BASE/f'storage/app/imm/imm5406{suffix}.pdf'
            for partner,status,expected in [(False,'2','hidden'),(True,'5','visible'),(False,'8','hidden'),(False,None,'hidden')]:
                payload={'form':'IMM5406','language':lang,'fields':{path:status} if status else {},'imm5406_rows':{},'imm5406_state':{'partner':partner}}
                with tempfile.TemporaryDirectory(dir=BASE/'storage/app/imm/tmp') as work:
                    output=Path(work)/'draft.pdf'
                    fill_family_information(payload,output,blank)
                    with pikepdf.open(blank) as original,pikepdf.open(output) as pdf:
                        def packet(document,name):
                            x=document.Root.AcroForm.XFA
                            return next(x[i+1].read_bytes() for i in range(0,len(x),2) if str(x[i])==name)
                        self.assertEqual(packet(original,'template'),packet(pdf,'template'))
                        form=E.fromstring(packet(pdf,'form'))
                        events=form.xpath('//*[local-name()="event" and @activity="docReady" and @ref="$host"]')
                        self.assertEqual(len(events),1)
                        if status:self.assertIn('execEvent("exit")',''.join(events[0].itertext()))
                        runtime=os.environ.get('IMM5406_XFA_RUNTIME')
                        if not runtime:continue
                        # Reproduce Reader losing visibility during host merge.
                        # The open event must restore it without relying on the delta.
                        form.xpath('//*[@name="Spouse"]')[0].set('presence','visible' if expected=='hidden' else 'hidden')
                        x=pdf.Root.AcroForm.XFA
                        for i in range(0,len(x),2):
                            if str(x[i])=='form':x[i+1]=pikepdf.Stream(pdf,E.tostring(form))
                        pdf.save(Path(work)/'merge.pdf')
                    sys.path.insert(0,runtime)
                    import pypdfium2_raw.bindings as raw
                    raw.FPDF_ERR_XFALOAD=7;raw.FPDF_ERR_XFALAYOUT=8
                    import pypdfium2 as pdfium
                    with pdfium.PdfDocument(Path(work)/'merge.pdf') as doc:
                        doc.init_forms();doc.save(Path(work)/'native.pdf')
                    with pikepdf.open(Path(work)/'native.pdf') as native:
                        f=E.fromstring(packet(native,'form'))
                        self.assertEqual(f.xpath('//*[@name="Spouse"]/@presence'),[expected])
                    if not partner and status=='2':
                        # A later manual marital-status change must not be
                        # overwritten by the questionnaire's older No answer.
                        from imm5406_pdf import resolve
                        with pikepdf.open(Path(work)/'merge.pdf') as edited:
                            data=E.fromstring(packet(edited,'datasets'))
                            resolve(data,path).text='5'
                            x=edited.Root.AcroForm.XFA
                            for i in range(0,len(x),2):
                                if str(x[i])=='datasets':x[i+1]=pikepdf.Stream(edited,E.tostring(data))
                            edited.save(Path(work)/'edited.pdf')
                        with pdfium.PdfDocument(Path(work)/'edited.pdf') as doc:
                            doc.init_forms();doc.save(Path(work)/'edited-native.pdf')
                        with pikepdf.open(Path(work)/'edited-native.pdf') as edited:
                            f=E.fromstring(packet(edited,'form'))
                            self.assertEqual(f.xpath('//*[@name="Spouse"]/@presence'),['visible'])

    def test_no_partner_preserves_signed_template_and_saves_visibility_only(self):
        from imm5406_pdf import fill_family_information
        for lang,suffix in [('en','e'),('fr','f')]:
            c=json.loads((BASE/f'resources/imm/imm5406-contract.{lang}.json').read_text(encoding='utf8'))
            blank=BASE/f'storage/app/imm/imm5406{suffix}.pdf'
            payload={'form':'IMM5406','language':lang,'fields':{c['groups']['applicant']['fields']['marital_status']['path']:'2'},'imm5406_rows':{},'imm5406_state':{'partner':False}}
            with tempfile.TemporaryDirectory(dir=BASE/'storage/app/imm/tmp') as work:
                out=Path(work)/'single.pdf'
                fill_family_information(payload,out,blank)
                def packets(path):
                    with pikepdf.open(path) as pdf:
                        x=pdf.Root.AcroForm.XFA
                        return {str(x[i]):x[i+1].read_bytes() for i in range(0,len(x),2)}
                original,generated=packets(blank),packets(out)
                self.assertEqual(generated['template'],original['template'])
                f=E.fromstring(generated['form'])
                self.assertEqual(f.xpath('//*[local-name()="subform" and @name="Spouse"]/@presence'),['hidden'])
                for name,value in original.items():
                    if name not in ('datasets','form'):self.assertEqual(value,generated[name],name)
                self.assertTrue(out.read_bytes().startswith(blank.read_bytes()))
                from imm0008_xfa_checksum import form_checksum
                self.assertEqual(E.fromstring(generated['form']).get('checksum'),form_checksum(generated['template'],generated['datasets']))

    def test_bilingual_standalone_seed_and_reviewed_contracts(self):
        seed = json.loads((BASE/'database/data/family_information_hub_questionnaire.json').read_text(encoding='utf8'))
        self.assertEqual(seed['questionnaire_key'], 'family_information_hub')
        for lang, suffix in [('en','e'), ('fr','f')]:
            contract = json.loads((BASE/f'resources/imm/imm5406-contract.{lang}.json').read_text(encoding='utf8'))
            self.assertEqual(contract['sha256'], hashlib.sha256((BASE/f'storage/app/imm/imm5406{suffix}.pdf').read_bytes()).hexdigest())
            self.assertEqual(contract['groups']['children']['initial'], 3)
            self.assertEqual(contract['groups']['children']['max'], -1)
            self.assertEqual(contract['groups']['siblings']['choices']['relationship']['step_brother'], '7' if lang=='fr' else '3')

    def test_dynamic_rows_choices_and_packet_preservation(self):
        from imm5406_pdf import fill_family_information, resolve
        for lang, suffix in [('en','e'), ('fr','f')]:
            c = json.loads((BASE/f'resources/imm/imm5406-contract.{lang}.json').read_text(encoding='utf8'))
            fields,metadata = {},{}
            for group,count in [('applicant',1),('spouse',1),('children',7),('siblings',8)]:
                for index in range(count):
                    for column,meta in c['groups'][group]['fields'].items():
                        value = '1' if column=='marriage_present' else '5' if column=='marital_status' else '2000-01-01' if column=='dob' else '3' if column=='relationship' else f'{group} {index} {column}'
                        if meta.get('allowed_values') and value not in meta['allowed_values']:value=meta['allowed_values'][0]
                        path=meta['path'].replace('{index}',str(index))
                        fields[path] = value
                        metadata[path] = meta
            payload={'form':'IMM5406','language':lang,'fields':fields,'imm5406_rows':{'children':7,'siblings':8},'imm5406_state':{'partner':True,'applicant_married':True,'spouse_married':True,'children':True,'siblings':True}}
            blank=BASE/f'storage/app/imm/imm5406{suffix}.pdf'
            def packets(path):
                with pikepdf.open(path) as pdf:
                    x=pdf.Root.AcroForm.XFA
                    return {str(x[i]):x[i+1].read_bytes() for i in range(0,len(x),2)}
            with tempfile.TemporaryDirectory(dir=BASE/'storage/app/imm/tmp') as work:
                output=Path(work)/'draft.pdf'
                fill_family_information(payload,output,blank)
                self.assertTrue(output.read_bytes().startswith(blank.read_bytes()))
                original,generated=packets(blank),packets(output)
                for name,raw in original.items():
                    if name not in ('form','datasets'):self.assertEqual(raw,generated[name],name)
                data,form=E.fromstring(generated['datasets']),E.fromstring(generated['form'])
                self.assertEqual(form.xpath('//*[local-name()="exclGroup"]/*[local-name()="value"]'),[])
                for path,value in fields.items():self.assertEqual(resolve(data,path).text,value,path)
                self.assertEqual(len(data.xpath('//*[local-name()="Child"]')),7)
                self.assertEqual(len(data.xpath('//*[local-name()="Sibling"]')),8)
                # Without instance managers viewers discard newly created Section A state.
                for node in form.xpath('//*[local-name()="subform"]'):
                    parent=node.getparent()
                    if E.QName(parent).localname=='form' or not node.get('name'):continue
                    self.assertTrue(any(E.QName(c).localname=='instanceManager' and c.get('name')=='_'+node.get('name') for c in parent),node.get('name'))
                for group in ('applicant','spouse'):
                    path=c['groups'][group]['fields']['marriage_present']['path'].rsplit('.',2)[0]
                    self.assertEqual(resolve(form,path,kinds=['subform']*len(path.split('.'))).get('presence'),'visible')
                # Optional independent XFA engine roundtrip. Dataset-only checks
                # cannot detect saved visibility being discarded during merge.
                runtime=os.environ.get('IMM5406_XFA_RUNTIME')
                if runtime:
                    sys.path.insert(0,runtime)
                    import pypdfium2_raw.bindings as raw
                    raw.FPDF_ERR_XFALOAD=7;raw.FPDF_ERR_XFALAYOUT=8
                    import pypdfium2 as pdfium
                    with pdfium.PdfDocument(output) as document:
                        document.init_forms()
                        document.save(Path(work)/'native.pdf')
                    native=packets(Path(work)/'native.pdf')
                    native_data,native_form=E.fromstring(native['datasets']),E.fromstring(native['form'])
                    for path,value in fields.items():
                        self.assertEqual((resolve(native_data,path).text or '').strip(),value,path)
                        meta=metadata[path]
                        if meta['kinds'][-1]=='exclGroup':continue
                        widget=resolve(native_form,path,kinds=meta['kinds'])
                        self.assertEqual(''.join(widget.xpath('./*[local-name()="value"]/*/text()')).strip(),value,path)
                    for group in ('applicant','spouse'):
                        path=c['groups'][group]['fields']['marriage_present']['path'].rsplit('.',2)[0]
                        self.assertEqual(resolve(native_form,path,kinds=['subform']*len(path.split('.'))).get('presence'),'visible')
                    applicant=c['groups']['applicant']
                    no_fields={path:value for path,value in fields.items() if path.startswith(applicant['container']+'.') and path!=applicant['fields']['marriage_present']['path']}
                    no_fields[applicant['fields']['marital_status']['path']]='2'
                    no_fields.update({path:'1' for path in c['selectors'].values()})
                    no_payload={**payload,'fields':no_fields,'imm5406_rows':{'children':0,'siblings':0},'imm5406_state':{key:False for key in payload['imm5406_state']}}
                    fill_family_information(no_payload,Path(work)/'no.pdf',blank)
                    with pdfium.PdfDocument(Path(work)/'no.pdf') as document:
                        document.init_forms();document.save(Path(work)/'no-native.pdf')
                    no_form=E.fromstring(packets(Path(work)/'no-native.pdf')['form'])
                    for group in ('spouse','children','siblings'):
                        path=c['groups'][group]['container'].replace('{index}','0')
                        self.assertEqual(resolve(no_form,path,kinds=['subform']*len(path.split('.'))).get('presence'),'hidden')
                with self.assertRaisesRegex(ValueError,'Unallowlisted'):
                    fill_family_information({**payload,'fields':{'IMM_5406.page1.SectionD.Signature.Signature':'fake'}},Path(work)/'bad.pdf',blank)
