import hashlib
import json
import subprocess
import sys
import unittest
from pathlib import Path

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


class Imm0008AssetsTest(unittest.TestCase):
    def test_templates_exhaustively_classified_and_repeats_bound(self):
        for lang, suffix, count in [('en', 'e', 360), ('fr', 'f', 359)]:
            contract = json.loads((BASE / f'resources/imm/imm0008-contract.{lang}.json').read_text(encoding='utf8'))
            self.assertEqual(contract['sha256'], hashlib.sha256((BASE / f'storage/app/imm/imm0008{suffix}.pdf').read_bytes()).hexdigest())
            self.assertEqual(contract['template_field_count'], count)
            self.assertEqual(contract['unmapped_visible_fields'], [])
            self.assertEqual(contract['dependant_capacity'], 5)
            self.assertTrue(all(v['data_path'] == k for k, v in contract['fields'].items()))
            self.assertTrue(any('dependants[3]' in k for k in contract['fields']))
            self.assertTrue(all('Consent1' not in k for k in contract['fields']))
            self.assertEqual(set(contract['program_categories']), {'01', '02', '03', '04'})

    def test_seed_is_bilingual_unlimited_and_isolated(self):
        seed = json.loads((BASE / 'database/data/permanent_residence_hub_questionnaire.json').read_text(encoding='utf8'))
        self.assertEqual(seed['questionnaire_key'], 'permanent_residence_hub')
        questions = [q for s in seed['sections'] for q in s['questions']]
        self.assertTrue(all(q['label_fr'] and q['source_key'].startswith('permanent_residence_hub.imm0008.') for q in questions))
        roster = next(q for q in questions if q['source_key'].endswith('.dependants'))
        self.assertEqual(roster['question_type'], 'repeatable_person')
        self.assertNotIn('maxRows', roster['field_schema'])
        self.assertIn('id', [c['id'] for c in roster['field_schema']['columns']])
        self.assertIn('principal_applicant_confirmed', [q['source_key'].split('.')[-1] for q in questions])

    def test_generation_is_idempotent_preserves_old_seeds_and_registry(self):
        paths = list((BASE / 'database/data').glob('*.json')) + [BASE / 'resources/imm/template_versions.json']
        before = {str(p): p.read_bytes() for p in paths}
        subprocess.run([sys.executable, str(BASE / 'resources/imm/build_imm0008_assets.py')], check=True, capture_output=True)
        self.assertEqual(before, {str(p): p.read_bytes() for p in paths})

    def test_choices_follow_widgets_and_all_dynamic_branches(self):
        for lang in ['en', 'fr']:
            contract = json.loads((BASE / f'resources/imm/imm0008-contract.{lang}.json').read_text(encoding='utf8'))
            fields = contract['fields']
            for path, meta in fields.items():
                if path.endswith(('DOBYYYY', 'DOBMM', 'DOBDD')):
                    self.assertNotIn('choices', meta, 'Stale bindItems on DOB text widgets must not become country choices')
            city = fields['form1.Page1.genDetails.q6.CityTown']
            self.assertEqual(len(city['city_choices']), 13)
            self.assertIn('Toronto', city['city_choices']['06'].values())
            self.assertNotIn('choices', city, 'Default Alberta items cannot constrain other provinces')
            residence = fields['form1.Page1.PersonalDetails.q13.PreviousCOR.Row2.Country']
            self.assertIn('.PrevCOR.', residence['template_path'])
            self.assertNotIn('.PrevCOR.', residence['data_path'])
            self.assertEqual(set(contract['program_categories']['03']), {'22', '23', '28'})
            self.assertIn('45', contract['program_categories']['04'])

    def test_questions_match_mapper_object_schema_and_semantic_kinds(self):
        seed = json.loads((BASE / 'database/data/permanent_residence_hub_questionnaire.json').read_text(encoding='utf8'))
        questions = {q['source_key'].removeprefix('permanent_residence_hub.imm0008.'): q for s in seed['sections'] for q in s['questions']}
        self.assertEqual({c['id'] for c in questions['phone']['field_schema']['fields']}, {'type', 'country_code', 'number', 'extension'})
        columns = {c['id']: c for c in questions['dependants']['field_schema']['columns']}
        self.assertEqual(columns['passport']['max_items'], 1)
        self.assertEqual(columns['eligibility_confirmed']['visibility_rule']['all'][0]['value'], ['02', '03'])
        rows = json.loads((BASE / 'resources/imm/maps/imm0008.json').read_text(encoding='utf8'))['fields']
        principal = {r['source'].removeprefix('permanent_residence_hub.imm0008.'): r for r in rows if r.get('row_group') != 'dependants'}
        self.assertEqual(principal['category']['kind'], 'category')
        self.assertEqual(principal['has_csq']['gate'], 'destination_province:05')
        self.assertTrue(principal['dob']['fallback'])
        self.assertTrue(all(not r['fallback'] for r in rows if r.get('row_group') == 'dependants'))

    def test_nested_client_labels_and_parent_visibility(self):
        seed = json.loads((BASE / 'database/data/permanent_residence_hub_questionnaire.json').read_text(encoding='utf8'))
        questions = {q['source_key'].split('.')[-1]: q for s in seed['sections'] for q in s['questions']}
        expected = {'country': ('Country of residence', 'Pays de résidence'), 'status': ('Immigration status', 'Statut d’immigration'), 'other_status': ('Exact status, if Other', 'Statut exact, si Autre'), 'from_date': ('Residence start date', 'Date de début de résidence'), 'to_date': ('Residence end date', 'Date de fin de résidence')}
        dep = {c['id']: c for c in questions['dependants']['field_schema']['columns']}
        for columns in [questions['previous_residences']['field_schema']['columns'], dep['previous_residences']['columns']]:
            self.assertEqual({c['id']: (c['label'], c['label_fr']) for c in columns}, expected)
        self.assertEqual(dep['out_of_status_confirmed']['visibility_rule'], {'all': [{'field': 'current_status', 'op': 'in', 'value': ['06']}]})
        self.assertEqual(dep['passport']['visibility_rule']['all'][0]['field'], 'has_passport')
        passport = {c['id']: c for c in dep['passport']['columns']}
        self.assertEqual(passport['taiwan_pin']['visibility_rule']['all'][0], {'field': 'country', 'op': 'in', 'value': ['203']})
        self.assertIn('optional', dep['id']['label'].lower())
        def audit(node):
            if isinstance(node, dict):
                if 'label' in node and ('id' in node or 'source_key' in node):
                    self.assertNotRegex(node['label'], r'\w\.\d|residences\.|partner\.|passport\.')
                    self.assertNotRegex(node.get('label_fr', ''), r'\w\.\d|residences\.|partner\.|passport\.')
                    self.assertNotEqual(node['label'], node.get('label_fr'), 'Client field must have a deliberate French label')
                for key, value in node.items():
                    if key != 'options': audit(value)
            elif isinstance(node, list):
                for value in node: audit(value)
        audit(seed)


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