import json
from pathlib import Path
from typing import Dict
from unittest.mock import Mock, call

import pytest
import yaml

from datamodel_code_generator import DataModelField
from datamodel_code_generator.model.pydantic import BaseModel, CustomRootType
from datamodel_code_generator.parser.base import dump_templates
from datamodel_code_generator.parser.jsonschema import (
    JsonSchemaObject,
    JsonSchemaParser,
    get_model_by_path,
)

DATA_PATH: Path = Path(__file__).parents[1] / 'data' / 'jsonschema'


@pytest.mark.parametrize(
    'schema,path,model',
    [
        ({'foo': 'bar'}, None, {'foo': 'bar'}),
        ({'a': {'foo': 'bar'}}, 'a', {'foo': 'bar'}),
        ({'a': {'b': {'foo': 'bar'}}}, 'a/b', {'foo': 'bar'}),
        ({'a': {'b': {'c': {'foo': 'bar'}}}}, 'a/b', {'c': {'foo': 'bar'}}),
        ({'a': {'b': {'c': {'foo': 'bar'}}}}, 'a/b/c', {'foo': 'bar'}),
    ],
)
def test_get_model_by_path(schema: Dict, path: str, model: Dict):
    assert get_model_by_path(schema, path.split('/') if path else []) == model


def test_json_schema_parser_parse_ref():
    parser = JsonSchemaParser(
        BaseModel, CustomRootType, data_model_field_type=DataModelField
    )
    parser.parse_raw_obj = Mock()
    external_parent_path = Path(DATA_PATH / 'external_parent.json')
    parser.base_path = external_parent_path.parent
    parser.excludes_ref_path = set()
    external_parent = external_parent_path.read_text()
    obj = JsonSchemaObject.parse_raw(external_parent)

    parser.parse_ref(obj, [])
    # parser.parse_raw_obj.assert_has_calls(
    #     [
    #         call(
    #             'Yaml',
    #             {'properties': {'firstName': {'type': 'string'}}, 'type': 'object'},
    #         ),
    #         call(
    #             'Json',
    #             {'properties': {'firstName': {'type': 'string'}}, 'type': 'object'},
    #         ),
    #     ]
    # )


def test_json_schema_object_ref_url_json(mocker):
    parser = JsonSchemaParser(
        BaseModel, CustomRootType, data_model_field_type=DataModelField
    )
    obj = JsonSchemaObject.parse_obj(
        {'$ref': 'https://example.org/schema.json#/definitions/User'}
    )
    mock_get = mocker.patch('httpx.get')
    mock_get.return_value.text = json.dumps(
        {
            "$id": "https://example.com/person.schema.json",
            "$schema": "http://json-schema.org/draft-07/schema#",
            "definitions": {
                "User": {"type": "object", "properties": {"name": {"type": "string",}},}
            },
        },
    )

    parser.parse_ref(obj, ['Model'])
    assert (
        dump_templates(list(parser.results))
        == '''class User(BaseModel):
    name: Optional[str] = None'''
    )
    parser.parse_ref(obj, ['Model'])
    mock_get.assert_called_once_with('https://example.org/schema.json',)


def test_json_schema_object_ref_url_yaml(mocker):
    parser = JsonSchemaParser(
        BaseModel, CustomRootType, data_model_field_type=DataModelField
    )
    obj = JsonSchemaObject.parse_obj(
        {'$ref': 'https://example.org/schema.yaml#/definitions/User'}
    )
    mock_get = mocker.patch('httpx.get')
    mock_get.return_value.text = yaml.safe_dump(
        json.load((DATA_PATH / 'user.json').open())
    )

    parser.parse_ref(obj, ['User'])
    assert (
        dump_templates(list(parser.results))
        == '''class User(BaseModel):
    name: Optional[str] = None'''
    )
    parser.parse_ref(obj, [])
    mock_get.assert_called_once_with('https://example.org/schema.yaml',)


def test_json_schema_object_cached_ref_url_yaml(mocker):
    parser = JsonSchemaParser(
        BaseModel, CustomRootType, data_model_field_type=DataModelField
    )

    obj = JsonSchemaObject.parse_obj(
        {
            'type': 'object',
            'properties': {
                'pet': {'$ref': 'https://example.org/schema.yaml#/definitions/Pet'},
                'user': {'$ref': 'https://example.org/schema.yaml#/definitions/User'},
            },
        }
    )
    mock_get = mocker.patch('httpx.get')
    mock_get.return_value.text = yaml.safe_dump(
        json.load((DATA_PATH / 'user.json').open())
    )

    parser.parse_ref(obj, [])
    assert (
        dump_templates(list(parser.results))
        == '''class Pet(BaseModel):
    name: Optional[str] = None


class User(BaseModel):
    name: Optional[str] = None'''
    )
    mock_get.assert_called_once_with('https://example.org/schema.yaml',)


def test_json_schema_ref_url_json(mocker):
    parser = JsonSchemaParser(
        BaseModel, CustomRootType, data_model_field_type=DataModelField
    )
    obj = {
        "type": "object",
        "properties": {
            "user": {'$ref': 'https://example.org/schema.json#/definitions/User'}
        },
    }
    mock_get = mocker.patch('httpx.get')
    mock_get.return_value.text = json.dumps(json.load((DATA_PATH / 'user.json').open()))

    parser.parse_raw_obj('Model', obj, ['Model'])
    assert (
        dump_templates(list(parser.results))
        == '''class Model(BaseModel):
    user: Optional[User] = None


class User(BaseModel):
    name: Optional[str] = None'''
    )
    mock_get.assert_called_once_with('https://example.org/schema.json',)


@pytest.mark.parametrize(
    'source_obj,generated_classes',
    [
        (
            {
                "$id": "https://example.com/person.schema.json",
                "$schema": "http://json-schema.org/draft-07/schema#",
                "title": "Person",
                "type": "object",
                "properties": {
                    "firstName": {
                        "type": "string",
                        "description": "The person's first name.",
                    },
                    "lastName": {
                        "type": "string",
                        "description": "The person's last name.",
                    },
                    "age": {
                        "description": "Age in years which must be equal to or greater than zero.",
                        "type": "integer",
                        "minimum": 0,
                    },
                },
            },
            """class Person(BaseModel):
    firstName: Optional[str] = None
    lastName: Optional[str] = None
    age: Optional[conint(ge=0)] = None""",
        ),
        (
            {
                "$id": "https://example.com/person.schema.json",
                "$schema": "http://json-schema.org/draft-07/schema#",
                "title": "person-object",
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "The person's name.",},
                    "home-address": {
                        "$ref": "#/definitions/home-address",
                        "description": "The person's home address.",
                    },
                },
                "definitions": {
                    "home-address": {
                        "type": "object",
                        "properties": {
                            "street-address": {"type": "string"},
                            "city": {"type": "string"},
                            "state": {"type": "string"},
                        },
                        "required": ["street_address", "city", "state"],
                    }
                },
            },
            """class Person(BaseModel):
    name: Optional[str] = None
    home_address: Optional[HomeAddress] = None""",
        ),
    ],
)
def test_parse_object(source_obj, generated_classes):
    parser = JsonSchemaParser(BaseModel, CustomRootType)
    parser.parse_object('Person', JsonSchemaObject.parse_obj(source_obj), [])
    assert dump_templates(list(parser.results)) == generated_classes


@pytest.mark.parametrize(
    'source_obj,generated_classes',
    [
        (
            {
                "$id": "https://example.com/person.schema.json",
                "$schema": "http://json-schema.org/draft-07/schema#",
                "title": "AnyJson",
                "description": "This field accepts any object",
            },
            """class AnyObject(BaseModel):
    __root__: Any""",
        )
    ],
)
def test_parse_any_root_object(source_obj, generated_classes):
    parser = JsonSchemaParser(
        BaseModel, CustomRootType, data_model_field_type=DataModelField
    )
    parser.parse_root_type('AnyObject', JsonSchemaObject.parse_obj(source_obj), [])
    assert dump_templates(list(parser.results)) == generated_classes


@pytest.mark.parametrize(
    'source_obj,generated_classes',
    [
        (
            yaml.safe_load((DATA_PATH / 'oneof.json').read_text()),
            (DATA_PATH / 'oneof.json.snapshot').read_text(),
        )
    ],
)
def test_parse_one_of_object(source_obj, generated_classes):
    parser = JsonSchemaParser(
        BaseModel, CustomRootType, data_model_field_type=DataModelField
    )
    parser.parse_raw_obj('onOfObject', source_obj, [])
    assert dump_templates(list(parser.results)) == generated_classes


@pytest.mark.parametrize(
    'source_obj,generated_classes',
    [
        (
            {
                "$id": "https://example.com/person.schema.json",
                "$schema": "http://json-schema.org/draft-07/schema#",
                "title": "defaults",
                "type": "object",
                "properties": {
                    "string": {"type": "string", "default": "default string",},
                    "string_on_field": {
                        "type": "string",
                        "default": "default string",
                        "description": "description",
                    },
                    "number": {"type": "number", "default": 123},
                    "number_on_field": {
                        "type": "number",
                        "default": 123,
                        "description": "description",
                    },
                    "number_array": {"type": "array", "default": [1, 2, 3]},
                    "string_array": {"type": "array", "default": ["a", "b", "c"]},
                    "object": {"type": "object", "default": {"key": "value"}},
                },
            },
            """class Defaults(BaseModel):
    string: Optional[str] = 'default string'
    string_on_field: Optional[str] = Field('default string', description='description')
    number: Optional[float] = 123
    number_on_field: Optional[float] = Field(123, description='description')
    number_array: Optional[List] = [1, 2, 3]
    string_array: Optional[List] = ['a', 'b', 'c']
    object: Optional[Dict[str, Any]] = {'key': 'value'}""",
        )
    ],
)
def test_parse_default(source_obj, generated_classes):
    parser = JsonSchemaParser(
        BaseModel, CustomRootType, data_model_field_type=DataModelField
    )
    parser.parse_raw_obj('Defaults', source_obj, [])
    assert dump_templates(list(parser.results)) == generated_classes


def test_parse_nested_array():
    parser = JsonSchemaParser(
        BaseModel,
        CustomRootType,
        data_model_field_type=DataModelField,
        text=(DATA_PATH / 'nested_array.json').read_text(),
    )
    parser.parse()
    assert (
        dump_templates(list(parser.results))
        == (DATA_PATH / 'nested_array.json.snapshot').read_text()
    )
