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

import pytest

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
from pydantic.typing import Literal


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'


@pytest.mark.skipif(not Literal, reason='typing_extensions not installed')
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'}
    ]


@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6')
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'}])"
    )
