import pickle
from typing import Dict, List, Optional, Union
from uuid import UUID, uuid4

import pytest
from typing_extensions import Literal

from pydantic import UUID1, BaseConfig, BaseModel, PydanticTypeError, ValidationError, conint, errors, validator
from pydantic.error_wrappers import flatten_errors, get_exc_type
from pydantic.errors import StrRegexError


def test_pydantic_error():
    class TestError(PydanticTypeError):
        code = 'test_code'
        msg_template = 'test message template "{test_ctx}"'

        def __init__(self, *, test_ctx: int) -> None:
            super().__init__(test_ctx=test_ctx)

    with pytest.raises(TestError) as exc_info:
        raise TestError(test_ctx='test_value')
    assert str(exc_info.value) == 'test message template "test_value"'


def test_pydantic_error_pickable():
    """
    Pydantic errors should be (un)pickable.
    (this test does not create a custom local error as we can't pickle local objects)
    """
    p = pickle.dumps(StrRegexError(pattern='pika'))
    error = pickle.loads(p)
    assert isinstance(error, StrRegexError)
    assert error.pattern == 'pika'


def test_interval_validation_error():
    class Foo(BaseModel):
        model_type: Literal['foo']
        f: int

    class Bar(BaseModel):
        model_type: Literal['bar']
        b: int

    class MyModel(BaseModel):
        foobar: Union[Foo, Bar]

        @validator('foobar', pre=True)
        def check_action(cls, v):
            if isinstance(v, dict):
                model_type = v.get('model_type')
                if model_type == 'foo':
                    return Foo(**v)
                if model_type == 'bar':
                    return Bar(**v)
            raise ValueError('not valid Foo or Bar')

    m1 = MyModel(foobar={'model_type': 'foo', 'f': '1'})
    assert m1.foobar.f == 1
    assert isinstance(m1.foobar, Foo)

    m2 = MyModel(foobar={'model_type': 'bar', 'b': '2'})
    assert m2.foobar.b == 2
    assert isinstance(m2.foobar, BaseModel)

    with pytest.raises(ValidationError) as exc_info:
        MyModel(foobar={'model_type': 'foo', 'f': 'x'})
    assert exc_info.value.errors() == [
        {'loc': ('foobar', 'f'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
    ]


def test_error_on_optional():
    class Foobar(BaseModel):
        foo: Optional[str] = None

        @validator('foo', always=True, pre=True)
        def check_foo(cls, v):
            raise ValueError('custom error')

    with pytest.raises(ValidationError) as exc_info:
        Foobar(foo='x')
    assert exc_info.value.errors() == [{'loc': ('foo',), 'msg': 'custom error', 'type': 'value_error'}]
    assert repr(exc_info.value.raw_errors[0]) == "ErrorWrapper(exc=ValueError('custom error'), loc=('foo',))"

    with pytest.raises(ValidationError) as exc_info:
        Foobar(foo=None)
    assert exc_info.value.errors() == [{'loc': ('foo',), 'msg': 'custom error', 'type': 'value_error'}]


@pytest.mark.parametrize(
    'result,expected',
    (
        (
            'errors',
            [
                {'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
                {'loc': ('b', 'x'), 'msg': 'field required', 'type': 'value_error.missing'},
                {'loc': ('b', 'z'), 'msg': 'field required', 'type': 'value_error.missing'},
                {'loc': ('c', 0, 'x'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
                {'loc': ('d',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
                {'loc': ('d',), 'msg': 'value is not a valid uuid', 'type': 'type_error.uuid'},
                {'loc': ('e', '__key__'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
                {'loc': ('f', 0), 'msg': 'none is not an allowed value', 'type': 'type_error.none.not_allowed'},
                {
                    'loc': ('g',),
                    'msg': 'uuid version 1 expected',
                    'type': 'value_error.uuid.version',
                    'ctx': {'required_version': 1},
                },
                {
                    'loc': ('h',),
                    'msg': 'yet another error message template 42',
                    'type': 'value_error.number.not_gt',
                    'ctx': {'limit_value': 42},
                },
            ],
        ),
        (
            'json',
            """\
[
  {
    "loc": [
      "a"
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  },
  {
    "loc": [
      "b",
      "x"
    ],
    "msg": "field required",
    "type": "value_error.missing"
  },
  {
    "loc": [
      "b",
      "z"
    ],
    "msg": "field required",
    "type": "value_error.missing"
  },
  {
    "loc": [
      "c",
      0,
      "x"
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  },
  {
    "loc": [
      "d"
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  },
  {
    "loc": [
      "d"
    ],
    "msg": "value is not a valid uuid",
    "type": "type_error.uuid"
  },
  {
    "loc": [
      "e",
      "__key__"
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  },
  {
    "loc": [
      "f",
      0
    ],
    "msg": "none is not an allowed value",
    "type": "type_error.none.not_allowed"
  },
  {
    "loc": [
      "g"
    ],
    "msg": "uuid version 1 expected",
    "type": "value_error.uuid.version",
    "ctx": {
      "required_version": 1
    }
  },
  {
    "loc": [
      "h"
    ],
    "msg": "yet another error message template 42",
    "type": "value_error.number.not_gt",
    "ctx": {
      "limit_value": 42
    }
  }
]""",
        ),
        (
            '__str__',
            """\
10 validation errors for Model
a
  value is not a valid integer (type=type_error.integer)
b -> x
  field required (type=value_error.missing)
b -> z
  field required (type=value_error.missing)
c -> 0 -> x
  value is not a valid integer (type=type_error.integer)
d
  value is not a valid integer (type=type_error.integer)
d
  value is not a valid uuid (type=type_error.uuid)
e -> __key__
  value is not a valid integer (type=type_error.integer)
f -> 0
  none is not an allowed value (type=type_error.none.not_allowed)
g
  uuid version 1 expected (type=value_error.uuid.version; required_version=1)
h
  yet another error message template 42 (type=value_error.number.not_gt; limit_value=42)""",
        ),
    ),
)
def test_validation_error(result, expected):
    class SubModel(BaseModel):
        x: int
        y: int
        z: str

    class Model(BaseModel):
        a: int
        b: SubModel
        c: List[SubModel]
        d: Union[int, UUID]
        e: Dict[int, str]
        f: List[Union[int, str]]
        g: UUID1
        h: conint(gt=42)

        class Config:
            error_msg_templates = {'value_error.number.not_gt': 'yet another error message template {limit_value}'}

    with pytest.raises(ValidationError) as exc_info:
        Model.parse_obj(
            {
                'a': 'not_int',
                'b': {'y': 42},
                'c': [{'x': 'not_int', 'y': 42, 'z': 'string'}],
                'd': 'string',
                'e': {'not_int': 'string'},
                'f': [None],
                'g': uuid4(),
                'h': 21,
            }
        )

    assert getattr(exc_info.value, result)() == expected


def test_errors_unknown_error_object():
    with pytest.raises(RuntimeError):
        list(flatten_errors([object], BaseConfig))


@pytest.mark.parametrize(
    'exc,type_',
    (
        (TypeError(), 'type_error'),
        (ValueError(), 'value_error'),
        (AssertionError(), 'assertion_error'),
        (errors.DecimalIsNotFiniteError(), 'value_error.decimal.not_finite'),
    ),
)
def test_get_exc_type(exc, type_):
    if isinstance(type_, str):
        assert get_exc_type(type(exc)) == type_
    else:
        with pytest.raises(type_) as exc_info:
            get_exc_type(type(exc))
        assert isinstance(exc_info.value, type_)


def test_single_error():
    class Model(BaseModel):
        x: int

    with pytest.raises(ValidationError) as exc_info:
        Model(x='x')

    expected = """\
1 validation error for Model
x
  value is not a valid integer (type=type_error.integer)"""
    assert str(exc_info.value) == expected
    assert str(exc_info.value) == expected  # to check lru cache doesn't break anything

    with pytest.raises(ValidationError) as exc_info:
        Model()

    assert (
        str(exc_info.value)
        == """\
1 validation error for Model
x
  field required (type=value_error.missing)"""
    )


def test_nested_error():
    class NestedModel3(BaseModel):
        x: str

    class NestedModel2(BaseModel):
        data2: List[NestedModel3]

    class NestedModel1(BaseModel):
        data1: List[NestedModel2]

    with pytest.raises(ValidationError) as exc_info:
        NestedModel1(data1=[{'data2': [{'y': 1}]}])

    expected = [{'loc': ('data1', 0, 'data2', 0, 'x'), 'msg': 'field required', 'type': 'value_error.missing'}]

    assert exc_info.value.errors() == expected


def test_validate_assignment_error():
    class Model(BaseModel):
        x: int

        class Config:
            validate_assignment = True

    model = Model(x=1)
    with pytest.raises(ValidationError) as exc_info:
        model.x = 'a'
    assert (
        str(exc_info.value)
        == '1 validation error for Model\nx\n  value is not a valid integer (type=type_error.integer)'
    )


def test_submodel_override_validation_error():
    class SubmodelA(BaseModel):
        x: str

    class SubmodelB(SubmodelA):
        x: int

    class Model(BaseModel):
        submodel: SubmodelB

    submodel = SubmodelA(x='a')
    with pytest.raises(ValidationError) as exc_info:
        Model(submodel=submodel)
    assert exc_info.value.errors() == [
        {'loc': ('submodel', 'x'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
    ]


def test_validation_error_methods():
    class Model(BaseModel):
        x: int

    with pytest.raises(ValidationError) as exc_info:
        Model(x='x')
    e = exc_info.value
    assert (
        str(e)
        == """\
1 validation error for Model
x
  value is not a valid integer (type=type_error.integer)"""
    )
    assert e.errors() == [{'loc': ('x',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}]
    assert e.json(indent=None) == (
        '[{"loc": ["x"], "msg": "value is not a valid integer", "type": "type_error.integer"}]'
    )
    assert repr(e) == (
        "ValidationError(model='Model', errors=[{'loc': ('x',), 'msg': 'value is not a valid integer', "
        "'type': 'type_error.integer'}])"
    )
