"""Laboratoire sans réseau : mapping fictif, pas un serveur ni un parseur Google.

Exécution : python3 verifier_migration_underscores.py
Les identités et réponses attendues sont des fixtures indépendantes du mapping.
"""
import copy
import json
from urllib.parse import urlsplit, urlunsplit

ORIGINS = {
    '/pieces/ab_cd': 'PIECE_A',
    '/pieces/ab-cd': 'PIECE_B',
    '/guides/pose_joint': 'GUIDE_C',
    '/guides/pose_joint_ancien': 'GUIDE_C',
    '/produits/kit_bleu?pack_id=AB_CD': 'KIT_D',
}
EXCLUDED = ['/recherche?ref=AB_CD', '/api/v1/order_status']
TARGETS = {
    '/pieces/ab-cd-a': 'PIECE_A',
    '/pieces/ab-cd': 'PIECE_B',
    '/guides/pose-joint': 'GUIDE_C',
    '/produits/kit-bleu?pack_id=AB_CD': 'KIT_D',
}
PAGES = {url: {'entity': entity, 'status': 200, 'canonical': url}
         for url, entity in TARGETS.items()}
MAPPING = {
    '/pieces/ab_cd': '/pieces/ab-cd-a',
    '/pieces/ab-cd': '/pieces/ab-cd',
    '/guides/pose_joint': '/guides/pose-joint',
    '/guides/pose_joint_ancien': '/guides/pose-joint',
    '/produits/kit_bleu?pack_id=AB_CD': '/produits/kit-bleu?pack_id=AB_CD',
}


def inspect(mapping, pages):
    errors = []
    if set(mapping) != set(ORIGINS):
        errors.append('source scope differs')
    destinations = {}
    for source, target in mapping.items():
        if source not in ORIGINS:
            errors.append('source outside HTML scope: ' + source)
            continue
        expected = ORIGINS[source]
        destinations.setdefault(target, set()).add(expected)
        if urlsplit(source).query != urlsplit(target).query:
            errors.append('query changed: ' + source)
        if target in mapping and mapping[target] != target:
            errors.append('indirect target or cycle: ' + source)
        page = pages.get(target)
        if page is None:
            errors.append('unknown target: ' + target)
            continue
        if page['entity'] != expected:
            errors.append('wrong entity: ' + source)
        if page['status'] != 200:
            errors.append('target not 200: ' + target)
        if page['canonical'] != target:
            errors.append('canonical differs: ' + target)
    for target, entities in destinations.items():
        if len(entities) > 1:
            errors.append('distinct entities collide: ' + target)
    return errors


def changed(mapping, source, target):
    result = dict(mapping)
    result[source] = target
    return result


def path_only(url):
    p = urlsplit(url)
    return urlunsplit((p.scheme, p.netloc, p.path.replace('_', '-'),
                       p.query, p.fragment))


def main():
    checks = 0

    def check(condition):
        nonlocal checks
        assert condition
        checks += 1

    check(inspect(MAPPING, PAGES) == [])
    check(len(MAPPING) == 5)
    redirects = {a: b for a, b in MAPPING.items() if a != b}
    check(len(redirects) == 4)
    check('/pieces/ab-cd' not in redirects)
    check(MAPPING['/guides/pose_joint'] == MAPPING['/guides/pose_joint_ancien'])
    for old, entity in ORIGINS.items():
        check(PAGES[MAPPING[old]]['entity'] == entity)
        check(urlsplit(old).query == urlsplit(MAPPING[old]).query)
    for excluded in EXCLUDED:
        check(excluded not in MAPPING)

    whole = {s: s.replace('_', '-') for s in ORIGINS}
    paths = {s: path_only(s) for s in ORIGINS}
    check(any('distinct entities collide' in x for x in inspect(whole, PAGES)))
    check(any('query changed' in x for x in inspect(whole, PAGES)))
    check(any('distinct entities collide' in x for x in inspect(paths, PAGES)))
    check(not any('query changed' in x for x in inspect(paths, PAGES)))

    bad_cases = []
    missing = dict(MAPPING)
    missing.pop('/guides/pose_joint_ancien')
    bad_cases.append(('source omitted', missing, PAGES))
    bad_cases.append(('collision', changed(MAPPING, '/pieces/ab_cd', '/pieces/ab-cd'), PAGES))
    bad_cases.append(('query key changed', changed(MAPPING, '/produits/kit_bleu?pack_id=AB_CD', '/produits/kit-bleu?pack-id=AB_CD'), PAGES))
    bad_cases.append(('query value changed', changed(MAPPING, '/produits/kit_bleu?pack_id=AB_CD', '/produits/kit-bleu?pack_id=AB-CD'), PAGES))
    bad_cases.append(('API route included', changed(MAPPING, '/api/v1/order_status', '/api/v1/order-status'), PAGES))
    bad_cases.append(('search route included', changed(MAPPING, '/recherche?ref=AB_CD', '/recherche?ref=AB-CD'), PAGES))
    bad_cases.append(('unknown target', changed(MAPPING, '/pieces/ab_cd', '/pieces/unknown'), PAGES))
    bad_cases.append(('chain', changed(MAPPING, '/guides/pose_joint_ancien', '/guides/pose_joint'), PAGES))
    cycle = changed(MAPPING, '/guides/pose_joint', '/guides/pose_joint_ancien')
    cycle['/guides/pose_joint_ancien'] = '/guides/pose_joint'
    bad_cases.append(('cycle', cycle, PAGES))
    for key, value, label in [('status', 404, '404 target'), ('canonical', '/pieces/ab-cd', 'wrong canonical'), ('entity', 'PIECE_B', 'wrong rendered entity')]:
        pages = copy.deepcopy(PAGES)
        pages['/pieces/ab-cd-a'][key] = value
        bad_cases.append((label, MAPPING, pages))
    before = json.dumps(MAPPING, sort_keys=True)
    rejected = {}
    for label, mapping, pages in bad_cases:
        errors = inspect(mapping, pages)
        check(bool(errors))
        rejected[label] = errors
    check(json.dumps(MAPPING, sort_keys=True) == before)
    check(120 * 2 == 240 and 240 / 60 == 4)
    print(json.dumps({'checks': checks, 'origins': 5, 'entities': 4,
                     'redirectsProposed': redirects, 'unchanged': '/pieces/ab-cd',
                     'excluded': EXCLUDED, 'rejectedCases': rejected,
                     'wholeReplacement': inspect(whole, PAGES),
                     'pathReplacement': inspect(paths, PAGES),
                     'networkCalls': 0, 'realRedirectsInstalled': 0},
                    ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()
