"""IMM5710 stored-XFA tests; Reader validation remains manual."""
import copy
import importlib.util
import json
from pathlib import Path
import unittest
from unittest.mock import patch
import hashlib
from uuid import uuid4

import pikepdf
from lxml import etree as E
from prefill_imm_xfa_pdf import fill

BASE = Path(__file__).resolve().parents[2]
NS = 'http://www.xfa.org/schema/xfa-data/1.0/'


def packets(path):
    with pikepdf.open(path) as pdf:
        a = pdf.Root.AcroForm.XFA
        return {str(a[i]): a[i + 1].read_bytes() for i in range(0, len(a), 2)}


def resolve(root, path, named=False):
    import re
    nodes = [root]
    for part in path.split('.'):
        match = re.fullmatch(r'([^\[\]]+)(?:\[(\d+)\])?', part)
        name, index = match.groups()
        candidates = [c for n in nodes for c in n if isinstance(c.tag, str) and
                      (c.get('name') if named else E.QName(c).localname) == name]
        nodes = candidates if index is None else candidates[int(index):int(index)+1]
    return nodes


class IMM5710WriterTest(unittest.TestCase):
    def setUp(self):
        self.work = BASE / 'storage/app/imm/tmp' / ('5710-test-' + uuid4().hex)
        self.work.mkdir(parents=True)

    def contract(self, language):
        return json.loads((BASE / f'resources/imm/imm5710-contract.{language}.json').read_text(encoding='utf-8'))

    def payload(self, language):
        contract = self.contract(language)
        fields = {}
        for index, (path, meta) in enumerate(contract['fields'].items()):
            value = 'VALUE' + str(index)
            if meta.get('allowed_values'):
                value = meta['allowed_values'][0]
            elif meta.get('choices'):
                value = next(k for k in meta['choices'] if k)
            elif meta.get('max_chars'):
                value = value[:meta['max_chars']]
            fields[path] = value
        fields['form1.Page2.ContactInformation.Resi.SameAsAddr.SameAsMailingInd'] = 'N'
        for path in fields:
            if path.endswith('CanOtherInd.Other'):
                fields[path] = '0'
        from imm5710_pdf_state import work_values
        fields = work_values(fields)
        return {'form': 'IMM5710', 'language': language, 'fields': fields}, contract

    def write(self, payload, blank, name='filled.pdf'):
        source = self.work / 'payload.json'
        source.write_text(json.dumps(payload), encoding='utf-8')
        output = self.work / name
        fill(str(source), str(output), str(blank))
        return output

    def test_isolated_writer_exists(self):
        self.assertIsNotNone(importlib.util.find_spec('imm5710_pdf'), 'IMM5710 requires isolated allowlisted writer')

    def test_bilingual_complete_payload_and_saved_state(self):
        for language, suffix in [('en', 'e'), ('fr', 'f')]:
            payload, contract = self.payload(language)
            snapshot = copy.deepcopy(payload)
            blank = BASE / f'storage/app/imm/imm5710{suffix}.pdf'
            before = packets(blank)
            output = self.write(payload, blank, language + '.pdf')
            actual = packets(output)
            self.assertEqual(payload, snapshot)
            for name, raw in before.items():
                if name not in ('datasets', 'form', 'template'):
                    self.assertEqual(actual[name], raw, name)
            for name in ('template', 'form'):
                old_scripts = E.fromstring(before[name]).xpath('//*[local-name()="script"]/text()')
                new_scripts = E.fromstring(actual[name]).xpath('//*[local-name()="script"]/text()')
                self.assertEqual(old_scripts, new_scripts)
            data = E.fromstring(actual['datasets']).find('{' + NS + '}data')
            saved = E.fromstring(actual['form'])
            for path, value in payload['fields'].items():
                nodes = resolve(data, path)
                self.assertEqual(len(nodes), 1, path)
                self.assertEqual(nodes[0].text or '', value, path)
                self.assertEqual(len(nodes[0]), 0, path)
                widgets = resolve(saved, contract['fields'][path]['template_path'], named=True)
                self.assertEqual(len(widgets), 1, path)
                texts = widgets[0].xpath('./*[local-name()="value"]/*')
                from imm5710_pdf import PHONE_PREFIXES
                if path.startswith(PHONE_PREFIXES):
                    self.assertFalse(texts, path)
                else:
                    self.assertEqual(''.join(texts[0].itertext()) if texts else '', value, path)
            self.assertEqual(packets(blank), before)

    def test_unknown_target_rejects_without_output(self):
        payload, _ = self.payload('en')
        payload['fields']['form1.Page1.UnknownTarget'] = 'INVALID'
        output = self.work / 'rejected.pdf'
        with self.assertRaisesRegex(ValueError, 'allowlist|allowlisted'):
            self.write(payload, BASE / 'storage/app/imm/imm5710e.pdf', output.name)
        self.assertFalse(output.exists())


    def test_mapper_shaped_phone_branches_bilingual(self):
        for language, suffix in [('en', 'e'), ('fr', 'f')]:
            for group, selected in [('Phone', 'CanadaUS'), ('AltPhone', 'Other')]:
                prefix = 'form1.Page2.ContactInformation.q3-4.' + group + '.'
                fields = {prefix + 'CanOtherInd.' + selected: '1', prefix + 'NumberCountry': '1' if selected == 'CanadaUS' else '94'}
                if selected == 'CanadaUS':
                    fields.update({prefix + 'NANumber.AreaCode': '416', prefix + 'NANumber.FirstThree': '555', prefix + 'NANumber.LastFive': '1234'})
                else:
                    fields[prefix + 'IntlNumber.IntlNumber'] = '771234567'
                payload = {'form': 'IMM5710', 'language': language, 'fields': fields}
                actual = packets(self.write(payload, BASE / f'storage/app/imm/imm5710{suffix}.pdf'))
                data = E.fromstring(actual['datasets']).find('{' + NS + '}data')
                for path, value in fields.items():
                    self.assertEqual(resolve(data, path)[0].text, value)
                for packet in ('template', 'form'):
                    root = E.fromstring(actual[packet])
                    self.assertEqual(resolve(root, prefix + 'NANumber', True)[0].get('presence'), 'visible' if selected == 'CanadaUS' else 'invisible')
                    self.assertEqual(resolve(root, prefix + 'IntlNumber', True)[0].get('presence'), 'invisible' if selected == 'CanadaUS' else 'visible')

    def test_work_rules_clear_inactive_only_and_preserve_facts(self):
        from imm5710_pdf_state import WORK
        for category in ('OWP', 'LMOS', 'Other'):
            fields = {WORK + 'Purpose.Type': category, WORK + 'Employer.Name': 'Confirmed employer', WORK + 'Purpose.Other': 'Confirmed category', WORK + 'Duration.LMO': '1234567', WORK + 'CAQ.CertNum': 'STALE'}
            payload = {'form': 'IMM5710', 'fields': fields}
            actual = packets(self.write(payload, BASE / 'storage/app/imm/imm5710e.pdf'))
            data = E.fromstring(actual['datasets']).find('{' + NS + '}data')
            self.assertEqual(resolve(data, WORK + 'Employer.Name')[0].text, 'Confirmed employer')
            for name, active in [('Purpose.Other', category == 'Other'), ('Duration.LMO', category == 'LMOS'), ('CAQ.CertNum', False)]:
                self.assertEqual(resolve(data, WORK + name)[0].text or '', fields[WORK + name] if active else '')
            for packet in ('template', 'form'):
                root = E.fromstring(actual[packet])
                field = resolve(root, WORK + 'Employer.Name', True)[0]
                self.assertEqual(field.get('access'), 'open')
                self.assertEqual(field.xpath('./*[local-name()="validate"]')[0].get('nullTest'), 'disabled' if category == 'OWP' else 'error')


    def test_same_address_clears_default_and_sets_group_exports(self):
        prefix = 'form1.Page2.ContactInformation.Resi.'
        for selection in ('Y', 'N'):
            fields = {prefix + 'SameAsAddr.SameAsMailingInd': selection, prefix + 'AddrLine1.Streetname': 'Confirmed street'}
            actual = packets(self.write({'form': 'IMM5710', 'fields': fields}, BASE / 'storage/app/imm/imm5710e.pdf'))
            data = E.fromstring(actual['datasets']).find('{' + NS + '}data')
            self.assertEqual(resolve(data, prefix + 'AddrLine1.Streetname')[0].text or '', '' if selection == 'Y' else 'Confirmed street')
            if selection == 'Y':
                self.assertEqual(resolve(data, prefix + 'AddrLine2.Country')[0].text or '', '')
            saved = E.fromstring(actual['form'])
            group = resolve(saved, prefix + 'SameAsAddr.SameAsMailingInd', True)[0]
            self.assertEqual(group.xpath('./*[local-name()="value"]/*/text()'), [selection])
            for name, export in [('Yes', 'Y'), ('No', 'N')]:
                child = resolve(group, name, True)[0]
                self.assertEqual(''.join(child.xpath('./*[local-name()="value"]/*/text()')), export if selection == export else '')
            for packet in ('template', 'form'):
                field = resolve(E.fromstring(actual[packet]), prefix + 'AddrLine1.Streetname', True)[0]
                self.assertEqual(field.get('access'), 'readOnly' if selection == 'Y' else 'open')


    def test_unresolved_contract_target_rejects_without_output(self):
        import imm5710_pdf
        contract = self.contract('en')
        path = next(iter(contract['fields']))
        contract['fields'][path]['template_path'] = 'form1.Page1.Unknown'
        with patch.object(imm5710_pdf, 'load_contract', return_value=contract):
            with self.assertRaisesRegex(ValueError, 'Unresolved'):
                self.write({'form': 'IMM5710', 'fields': {path: ''}}, BASE / 'storage/app/imm/imm5710e.pdf')
        self.assertFalse((self.work / 'filled.pdf').exists())

    def test_invalid_exports_types_and_manual_targets_reject(self):
        contract = self.contract('en')
        path = next(k for k, v in contract['fields'].items() if v.get('allowed_values'))
        for key, value in [(path, 'invalid'), (path, 1), (contract['manual_fields'][0], '1')]:
            with self.assertRaises(ValueError):
                self.write({'form': 'IMM5710', 'fields': {key: value}}, BASE / 'storage/app/imm/imm5710e.pdf')
            self.assertFalse((self.work / 'filled.pdf').exists())


    def test_stale_manual_values_cleared_without_stamping_draft(self):
        import imm5710_pdf
        contract = self.contract('en')
        blank = self.work / 'stale.pdf'
        source = BASE / 'storage/app/imm/imm5710e.pdf'
        with pikepdf.open(source) as pdf:
            a = pdf.Root.AcroForm.XFA
            streams = {str(a[i]): a[i+1] for i in range(0, len(a), 2)}
            for packet in ('datasets', 'form'):
                root = E.fromstring(streams[packet].read_bytes())
                base = root.find('{' + NS + '}data') if packet == 'datasets' else root
                for path in contract['manual_fields']:
                    nodes = resolve(base, path, packet == 'form')
                    if nodes:
                        if packet == 'datasets':
                            nodes[0].text = 'STALE'
                        else:
                            imm5710_pdf.form_value(nodes[0], 'STALE')
                streams[packet].write(E.tostring(root))
            pdf.save(blank)
        contract['sha256'] = hashlib.sha256(blank.read_bytes()).hexdigest()
        with patch.object(imm5710_pdf, 'load_contract', return_value=contract):
            actual = packets(self.write({'form': 'IMM5710', 'fields': {}}, blank))
        for packet in ('datasets', 'form'):
            root = E.fromstring(actual[packet])
            base = root.find('{' + NS + '}data') if packet == 'datasets' else root
            for path in contract['manual_fields']:
                for node in resolve(base, path, packet == 'form'):
                    if packet == 'datasets':
                        self.assertFalse(node.text)
                    else:
                        self.assertFalse(node.xpath('./*[local-name()="value"]/*/text()'))

    def test_contradictory_phone_rejects_without_output(self):
        prefix = 'form1.Page2.ContactInformation.q3-4.Phone.CanOtherInd.'
        payload = {'form': 'IMM5710', 'fields': {prefix + 'CanadaUS': '1', prefix + 'Other': '1'}}
        with self.assertRaisesRegex(ValueError, 'Contradictory'):
            self.write(payload, BASE / 'storage/app/imm/imm5710e.pdf')
        self.assertFalse((self.work / 'filled.pdf').exists())

if __name__ == '__main__':
    unittest.main()
