import json

import pytest
from jsonschema.validators import RefResolver

from swagger_spec_validator.common import SwaggerValidationError
from swagger_spec_validator.validator20 import validate_spec
from tests.validator20.conftest import get_spec_json_and_url


@pytest.fixture
def minimal_swagger_dict():
    """Return minimal dict that respresents a swagger spec - useful as a base
    template.
    """
    return {
        "swagger": "2.0",
        "info": {
            "title": "Test",
            "version": "1.0",
        },
        "paths": {},
        "definitions": {},
    }


def test_success(petstore_dict):
    assert isinstance(validate_spec(petstore_dict), RefResolver)


def test_definitons_not_present_success(minimal_swagger_dict):
    del minimal_swagger_dict["definitions"]
    validate_spec(minimal_swagger_dict)


def test_empty_definitions_success(minimal_swagger_dict):
    validate_spec(minimal_swagger_dict)


def test_api_parameters_as_refs():
    # Verify issue #29 - instragram.json comes from:
    #
    # http://editor.swagger.io/#/
    #    -> File
    #       -> Open Example...
    #          -> instagram.yaml
    #
    # and then export it to a json file.
    instagram_specs, _ = get_spec_json_and_url("./tests/data/v2.0/instagram.json")
    validate_spec(instagram_specs)


def test_fails_on_invalid_external_ref_in_dict():
    # The external ref in petstore.json is valid.
    # The contents of the external ref (pet.json#/getall) is not - the 'name'
    # key in the parameter is missing.

    petstore_spec, petstore_url = get_spec_json_and_url(
        "./tests/data/v2.0/test_fails_on_invalid_external_ref/petstore.json"
    )

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(petstore_spec, petstore_url)

    assert "is not valid under any of the given schemas" in str(excinfo.value)


def test_fails_on_invalid_external_ref_in_list():
    # The external ref in petstore.json is valid.
    # The contents of the external ref (pet.json#/get_all_parameters) is not
    # - the 'name' key in the parameter is missing.
    petstore_spec, petstore_url = get_spec_json_and_url(
        "./tests/data/v2.0/test_fails_on_invalid_external_ref_in_list/petstore.json"
    )

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(petstore_spec, petstore_url)

    assert "is not valid under any of the given schemas" in str(excinfo.value)


@pytest.fixture
def node_spec():
    """Used in tests that have recursive $refs"""
    return {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "child": {
                "$ref": "#/definitions/Node",
            },
        },
        "required": ["name"],
    }


def test_recursive_ref(minimal_swagger_dict, node_spec):
    minimal_swagger_dict["definitions"]["Node"] = node_spec
    validate_spec(minimal_swagger_dict)


def test_recursive_ref_failure(minimal_swagger_dict, node_spec):
    minimal_swagger_dict["definitions"]["Node"] = node_spec
    # insert non-existent $ref
    node_spec["properties"]["foo"] = {"$ref": "#/definitions/Foo"}
    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(minimal_swagger_dict)
    assert "Unresolvable JSON pointer" in str(excinfo.value)


def test_complicated_refs():
    # Split the swagger spec into a bunch of different json files and use
    # $refs all over to place to wire stuff together - see the test-data
    # files or this will make no sense whatsoever.
    file_path = "./tests/data/v2.0/test_complicated_refs/swagger.json"
    swagger_dict, origin_url = get_spec_json_and_url(file_path)
    resolver = validate_spec(swagger_dict, spec_url=origin_url)

    # Hokey verification but better than nothing:
    #   If all the files with $refs were ingested and validated and an
    #   exception was not thrown, there should be 7 cached file references
    #   in the resolver's store:
    #       6 json files from tests/data/v2.0/tests_complicated_refs/*
    #       1 yaml files from tests/data/v2.0/tests_complicated_refs/*
    assert len([uri for uri in resolver.store.keys() if uri.startswith("file://")]) == 7


def test_specs_with_discriminator():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    validate_spec(swagger_dict)


def test_specs_with_discriminator_fail_because_not_required():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    swagger_dict["definitions"]["GenericPet"]["discriminator"] = "name"

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(swagger_dict)
    assert "discriminator (name) must be a required property" in str(excinfo.value)


def test_specs_with_discriminator_fail_because_not_string():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    swagger_dict["definitions"]["GenericPet"]["discriminator"] = "weight"

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(swagger_dict)
    assert "discriminator (weight) must be a string property" in str(excinfo.value)


def test_specs_with_discriminator_fail_because_not_in_properties():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    swagger_dict["definitions"]["GenericPet"]["discriminator"] = "an_other_property"

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(swagger_dict)
    assert "discriminator (an_other_property) must be defined in properties" in str(
        excinfo.value
    )


def test_specs_with_discriminator_in_allOf():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    validate_spec(swagger_dict)


def test_specs_with_discriminator_in_allOf_fail_because_not_required():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    swagger_dict["definitions"]["BaseObject"]["discriminator"] = "name"

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(swagger_dict)
    assert "discriminator (name) must be a required property" in str(excinfo.value)


def test_specs_with_discriminator_in_allOf_fail_because_not_string():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    swagger_dict["definitions"]["BaseObject"]["discriminator"] = "weight"

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(swagger_dict)
    assert "discriminator (weight) must be a string property" in str(excinfo.value)


def test_specs_with_discriminator_in_allOf_fail_because_not_in_properties():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    swagger_dict["definitions"]["BaseObject"]["discriminator"] = "an_other_property"

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(swagger_dict)
    assert "discriminator (an_other_property) must be defined in properties" in str(
        excinfo.value
    )


def test_read_yaml_specs():
    file_path = "./tests/data/v2.0/test_polymorphic_specs/swagger.json"
    swagger_dict, _ = get_spec_json_and_url(file_path)

    swagger_dict["definitions"]["BaseObject"]["discriminator"] = "an_other_property"

    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(swagger_dict)
    assert "discriminator (an_other_property) must be defined in properties" in str(
        excinfo.value
    )


@pytest.fixture
def default_checks_spec_dict(minimal_swagger_dict):
    minimal_swagger_dict["definitions"]["bool_string"] = {
        "properties": {
            "value": {
                "type": "string",
                "enum": ["True", "False"],
            },
        },
    }
    return minimal_swagger_dict


@pytest.mark.parametrize(
    "property_spec",
    [
        {"type": "integer", "default": 1},
        {"type": "boolean", "default": True},
        {"type": "null", "default": None},
        {"type": "number", "default": 2},
        {"type": "number", "default": 3.4},
        {"type": "object", "default": {"a_random_property": "valid"}},
        {"type": "array", "items": {"type": "integer"}, "default": [5, 6, 7]},
        {"type": "string", "default": ""},
        {"type": "string", "default": None, "x-nullable": True},
        {"default": -1},  # if type is not defined any value is a valid value
        {
            "type": "array",
            "items": {"$ref": "#/definitions/bool_string"},
            "default": [{"value": "False"}],
        },
    ],
)
def test_valid_specs_with_check_of_default_types(
    default_checks_spec_dict, property_spec
):
    default_checks_spec_dict["definitions"]["injected_definition"] = {
        "properties": {"property": property_spec},
    }
    # Success if no exception are raised
    validate_spec(default_checks_spec_dict)


@pytest.mark.parametrize(
    "property_spec, validator, instance",
    [
        [
            {"type": "integer", "default": "wrong_type"},
            "type",
            "wrong_type",
        ],
        [
            {"type": "boolean", "default": "wrong_type"},
            "type",
            "wrong_type",
        ],
        [
            {"type": "null", "default": "wrong_type"},
            "type",
            "wrong_type",
        ],
        [
            {"type": "number", "default": "wrong_type"},
            "type",
            "wrong_type",
        ],
        [
            {"type": "object", "default": "wrong_type"},
            "type",
            "wrong_type",
        ],
        [
            {"type": "array", "default": "wrong_type"},
            "type",
            "wrong_type",
        ],
        [
            {"type": "string", "default": -1},
            "type",
            -1,
        ],
        [
            {"type": "string", "minLength": 100, "default": "short_string"},
            "minLength",
            "short_string",
        ],
        [
            {"type": ["number", "boolean"], "default": "not_a_number_or_boolean"},
            "type",
            "not_a_number_or_boolean",
        ],
        [
            {
                "type": "array",
                "items": {"$ref": "#/definitions/bool_string"},
                "default": [{"value": "not_valid"}],
            },
            "enum",
            "not_valid",
        ],
    ],
)
def test_failure_due_to_wrong_default_type(
    default_checks_spec_dict, property_spec, validator, instance
):
    default_checks_spec_dict["definitions"]["injected_definition"] = {
        "properties": {"property": property_spec},
    }
    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(default_checks_spec_dict)

    validation_error = excinfo.value.args[1]
    assert validation_error.instance == instance
    assert validation_error.validator == validator


def test_ref_without_str_argument(minimal_swagger_dict):
    not_a_ref = {"properties": {"$ref": {"type": "string"}}}
    minimal_swagger_dict["definitions"]["not_a_ref"] = not_a_ref
    validate_spec(minimal_swagger_dict)


def test_failure_because_references_in_operation_responses():
    with open(
        "./tests/data/v2.0/invalid_swagger_specs_because_ref_in_responses.json"
    ) as f:
        invalid_spec = json.load(f)
    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(invalid_spec)
    assert (
        "GET /endpoint does not have a valid responses section. "
        "That section cannot be just a reference to another object."
        in str(excinfo.value)
    )


def test_type_array_with_items_succeed_validation(minimal_swagger_dict):
    minimal_swagger_dict["definitions"] = {
        "definition_1": {
            "type": "array",
            "items": {
                "type": "string",
            },
        },
    }

    # Success if no exception are raised
    validate_spec(minimal_swagger_dict)


@pytest.mark.parametrize(
    "swagger_dict_override",
    (
        {
            "definitions": {
                "definition_1": {
                    "type": "array",
                },
            },
        },
    ),
)
def test_type_array_without_items_succeed_fails(
    minimal_swagger_dict, swagger_dict_override
):
    minimal_swagger_dict.update(swagger_dict_override)
    with pytest.raises(SwaggerValidationError) as excinfo:
        validate_spec(minimal_swagger_dict)

    assert (
        str(excinfo.value)
        == "Definition of type array must define `items` property (definition #/definitions/definition_1)."
    )


INVALID_SCHEMA_OBJECT = {
    "type": "object",
    "properties": {"prop": {"type": "string", "default": 1}},
}


@pytest.mark.parametrize(
    "swagger_dict_override",
    (
        {
            "definitions": {"definition_1": INVALID_SCHEMA_OBJECT},
        },
        {
            "definitions": {
                "definition_2": {
                    "type": "object",
                    "properties": {"prop": INVALID_SCHEMA_OBJECT},
                }
            },
        },
        {
            "parameters": {
                "param_body": {
                    "in": "body",
                    "name": "body",
                    "schema": INVALID_SCHEMA_OBJECT,
                },
            },
        },
        {
            "paths": {
                "/endpoint": {
                    "get": {
                        "parameters": [
                            {
                                "in": "body",
                                "name": "body",
                                "schema": INVALID_SCHEMA_OBJECT,
                            }
                        ],
                        "responses": {
                            "200": {"description": "desc"},
                        },
                    },
                },
            },
        },
        {
            "paths": {
                "/endpoint": {
                    "get": {
                        "responses": {
                            "200": {
                                "description": "desc",
                                "schema": INVALID_SCHEMA_OBJECT,
                            },
                        },
                    },
                },
            },
        },
    ),
)
def test_highlight_inconsistent_schema_object_validation(
    minimal_swagger_dict, swagger_dict_override
):
    minimal_swagger_dict.update(swagger_dict_override)
    with pytest.raises(SwaggerValidationError):
        validate_spec(minimal_swagger_dict)


def test_additional_properties_validated(
    minimal_swagger_dict, default_checks_spec_dict
):
    invalid_spec = {"type": "object", "required": ["foo"]}
    minimal_swagger_dict["definitions"]["injected_definition"] = {
        "type": "object",
        "additionalProperties": invalid_spec,
    }

    with pytest.raises(SwaggerValidationError):
        validate_spec(minimal_swagger_dict)


def test_additional_properties_bool(minimal_swagger_dict, default_checks_spec_dict):
    minimal_swagger_dict["definitions"]["injected_definition"] = {
        "type": "object",
        "additionalProperties": False,
    }

    validate_spec(minimal_swagger_dict)


def test_array_items_validated(minimal_swagger_dict, default_checks_spec_dict):
    invalid_spec = {"type": "object", "required": ["foo"]}
    minimal_swagger_dict["definitions"]["injected_definition"] = {
        "type": "array",
        "items": invalid_spec,
    }

    with pytest.raises(SwaggerValidationError):
        validate_spec(minimal_swagger_dict)
