#!/usr/bin/env python3

import json, sys


def assert_non_empty_string(obj, field):
    assert field in obj, 'Missing field "%s"' % field
    assert isinstance(obj[field], str), \
        'Field "%s" must be a string' % field
    assert len(obj[field]) > 0, 'Field "%s" must not be empty' % field


def assert_non_empty_list(obj, field):
    assert isinstance(obj[field], list), \
        '%s must be a list' % field
    assert len(obj[field]) > 0, \
        '%s list must not be empty' % field


def assert_non_empty_dict(obj, field):
    assert isinstance(obj[field], dict), \
        '%s must be a dict' % field
    assert len(obj[field]) > 0, \
        '%s dict must not be empty' % field


def assert_contains(obj, field):
    assert field in obj, 'Must contain field "%s"' % field


def assert_value_from(obj, field, items):
    assert obj[field] in items, \
         'Field "%s" must be from: %s' % (field, str(items))


def assert_atom_or_list_items_from(obj, field, items):
    if isinstance(obj[field], str) or isinstance(
            obj[field], int) or obj[field] is None:
        assert_value_from(obj, field, items)
        return

    assert isinstance(obj[field], list), '%s must be a list' % field
    for allowed_value in obj[field]:
        assert allowed_value != '*', "Wildcard is not supported for lists!"
        assert allowed_value in items, \
            'Field "%s" must be from: %s' % (field, str(items))


def assert_contains_only_fields(obj, expected_fields):
    for expected_field in expected_fields:
        assert_contains(obj, expected_field)

    for actual_field in obj:
        assert actual_field in expected_fields, \
                'Unexpected field "%s".' % actual_field


def leaf_values(schema):
    if isinstance(schema, list):
        return schema
    ret = []
    for _, sub_schema in schema.iteritems():
        ret += leaf_values(sub_schema)
    return ret


def assert_value_unique_in(value, used_values):
    assert value not in used_values, 'Duplicate value "%s"!' % str(value)
    used_values[value] = True


def assert_valid_artifact(exp_pattern, artifact_key, schema):
    if isinstance(schema, list):
        assert_atom_or_list_items_from(exp_pattern, artifact_key,
                                       ["*"] + schema)
        return

    for sub_artifact_key, sub_schema in schema.iteritems():
        assert_valid_artifact(exp_pattern[artifact_key], sub_artifact_key,
                              sub_schema)


def validate(spec_json, details):
    """ Validates the json specification for generating tests. """

    details['object'] = spec_json
    assert_contains_only_fields(spec_json, [
        "selection_pattern", "test_file_path_pattern",
        "test_description_template", "test_page_title_template",
        "specification", "delivery_key", "subresource_schema",
        "source_context_schema", "source_context_list_schema",
        "test_expansion_schema", "excluded_tests"
    ])
    assert_non_empty_list(spec_json, "specification")
    assert_non_empty_dict(spec_json, "test_expansion_schema")
    assert_non_empty_list(spec_json, "excluded_tests")

    specification = spec_json['specification']
    test_expansion_schema = spec_json['test_expansion_schema']
    excluded_tests = spec_json['excluded_tests']

    valid_test_expansion_fields = test_expansion_schema.keys()

    # Should be consistent with `sourceContextMap` in
    # `/common/security-features/resources/common.sub.js`.
    valid_source_context_names = [
        "top", "iframe", "iframe-blank", "srcdoc", "worker-classic",
        "worker-module", "worker-classic-data", "worker-module-data",
        "sharedworker-classic", "sharedworker-module",
        "sharedworker-classic-data", "sharedworker-module-data"
    ]

    valid_subresource_names = [
        "a-tag", "area-tag", "audio-tag", "form-tag", "iframe-tag", "img-tag",
        "link-css-tag", "link-prefetch-tag", "object-tag", "picture-tag",
        "script-tag", "script-tag-dynamic-import", "video-tag"
    ] + ["beacon", "fetch", "xhr", "websocket"] + [
        "worker-classic", "worker-module", "worker-import",
        "worker-import-data", "sharedworker-classic", "sharedworker-module",
        "sharedworker-import", "sharedworker-import-data",
        "serviceworker-classic", "serviceworker-module",
        "serviceworker-import", "serviceworker-import-data"
    ] + [
        "worklet-animation", "worklet-audio", "worklet-layout",
        "worklet-paint", "worklet-animation-import", "worklet-audio-import",
        "worklet-layout-import", "worklet-paint-import",
        "worklet-animation-import-data", "worklet-audio-import-data",
        "worklet-layout-import-data", "worklet-paint-import-data"
    ]

    # Validate each single spec.
    for spec in specification:
        details['object'] = spec

        # Validate required fields for a single spec.
        assert_contains_only_fields(spec, [
            'title', 'description', 'specification_url', 'test_expansion'
        ])
        assert_non_empty_string(spec, 'title')
        assert_non_empty_string(spec, 'description')
        assert_non_empty_string(spec, 'specification_url')
        assert_non_empty_list(spec, 'test_expansion')

        for spec_exp in spec['test_expansion']:
            details['object'] = spec_exp
            assert_contains_only_fields(spec_exp, valid_test_expansion_fields)

            for artifact in test_expansion_schema:
                details['test_expansion_field'] = artifact
                assert_valid_artifact(spec_exp, artifact,
                                      test_expansion_schema[artifact])
                del details['test_expansion_field']

    # Validate source_context_schema.
    details['object'] = spec_json['source_context_schema']
    assert_contains_only_fields(
        spec_json['source_context_schema'],
        ['supported_delivery_type', 'supported_subresource'])
    assert_contains_only_fields(
        spec_json['source_context_schema']['supported_delivery_type'],
        valid_source_context_names)
    for source_context in spec_json['source_context_schema'][
            'supported_delivery_type']:
        assert_valid_artifact(
            spec_json['source_context_schema']['supported_delivery_type'],
            source_context, test_expansion_schema['delivery_type'])
    assert_contains_only_fields(
        spec_json['source_context_schema']['supported_subresource'],
        valid_source_context_names)
    for source_context in spec_json['source_context_schema'][
            'supported_subresource']:
        assert_valid_artifact(
            spec_json['source_context_schema']['supported_subresource'],
            source_context, leaf_values(test_expansion_schema['subresource']))

    # Validate subresource_schema.
    details['object'] = spec_json['subresource_schema']
    assert_contains_only_fields(spec_json['subresource_schema'],
                                ['supported_delivery_type'])
    assert_contains_only_fields(
        spec_json['subresource_schema']['supported_delivery_type'],
        leaf_values(test_expansion_schema['subresource']))
    for subresource in spec_json['subresource_schema'][
            'supported_delivery_type']:
        assert_valid_artifact(
            spec_json['subresource_schema']['supported_delivery_type'],
            subresource, test_expansion_schema['delivery_type'])

    # Validate the test_expansion schema members.
    details['object'] = test_expansion_schema
    assert_contains_only_fields(test_expansion_schema, [
        'expansion', 'source_scheme', 'source_context_list', 'delivery_type',
        'delivery_value', 'redirection', 'subresource', 'origin', 'expectation'
    ])
    assert_atom_or_list_items_from(test_expansion_schema, 'expansion',
                                   ['default', 'override'])
    assert_atom_or_list_items_from(test_expansion_schema, 'source_scheme',
                                   ['http', 'https'])
    assert_atom_or_list_items_from(
        test_expansion_schema, 'source_context_list',
        spec_json['source_context_list_schema'].keys())

    # Should be consistent with `preprocess_redirection` in
    # `/common/security-features/subresource/subresource.py`.
    assert_atom_or_list_items_from(test_expansion_schema, 'redirection', [
        'no-redirect', 'keep-origin', 'swap-origin', 'keep-scheme',
        'swap-scheme', 'downgrade'
    ])
    for subresource in leaf_values(test_expansion_schema['subresource']):
        assert subresource in valid_subresource_names, "Invalid subresource %s" % subresource
    # Should be consistent with getSubresourceOrigin() in
    # `/common/security-features/resources/common.sub.js`.
    assert_atom_or_list_items_from(test_expansion_schema, 'origin', [
        'same-http', 'same-https', 'same-ws', 'same-wss', 'cross-http',
        'cross-https', 'cross-ws', 'cross-wss', 'same-http-downgrade',
        'cross-http-downgrade', 'same-ws-downgrade', 'cross-ws-downgrade'
    ])

    # Validate excluded tests.
    details['object'] = excluded_tests
    for excluded_test_expansion in excluded_tests:
        assert_contains_only_fields(excluded_test_expansion,
                                    valid_test_expansion_fields)
        details['object'] = excluded_test_expansion
        for artifact in test_expansion_schema:
            details['test_expansion_field'] = artifact
            assert_valid_artifact(excluded_test_expansion, artifact,
                                  test_expansion_schema[artifact])
            del details['test_expansion_field']

    del details['object']


def assert_valid_spec_json(spec_json):
    error_details = {}
    try:
        validate(spec_json, error_details)
    except AssertionError as err:
        print('ERROR:', err)
        print(json.dumps(error_details, indent=4))
        sys.exit(1)


def main():
    spec_json = load_spec_json()
    assert_valid_spec_json(spec_json)
    print("Spec JSON is valid.")


if __name__ == '__main__':
    main()
