import re
from typing import Any, List, Optional

import pytest

from pydantic import BaseConfig, BaseModel, Extra, ValidationError
from pydantic.fields import Field


def test_alias_generator():
    def to_camel(string: str):
        return ''.join(x.capitalize() for x in string.split('_'))

    class MyModel(BaseModel):
        a: List[str] = None
        foo_bar: str

        class Config:
            alias_generator = to_camel

    data = {'A': ['foo', 'bar'], 'FooBar': 'foobar'}
    v = MyModel(**data)
    assert v.a == ['foo', 'bar']
    assert v.foo_bar == 'foobar'
    assert v.dict(by_alias=True) == data


def test_alias_generator_with_field_schema():
    def to_upper_case(string: str):
        return string.upper()

    class MyModel(BaseModel):
        my_shiny_field: Any  # Alias from Config.fields will be used
        foo_bar: str  # Alias from Config.fields will be used
        baz_bar: str  # Alias will be generated
        another_field: str  # Alias will be generated

        class Config:
            alias_generator = to_upper_case
            fields = {'my_shiny_field': 'MY_FIELD', 'foo_bar': {'alias': 'FOO'}, 'another_field': {'not_alias': 'a'}}

    data = {'MY_FIELD': ['a'], 'FOO': 'bar', 'BAZ_BAR': 'ok', 'ANOTHER_FIELD': '...'}
    m = MyModel(**data)
    assert m.dict(by_alias=True) == data


def test_alias_generator_wrong_type_error():
    def return_bytes(string):
        return b'not a string'

    with pytest.raises(TypeError) as e:

        class MyModel(BaseModel):
            bar: Any

            class Config:
                alias_generator = return_bytes

    assert str(e.value) == "Config.alias_generator must return str, not <class 'bytes'>"


def test_infer_alias():
    class Model(BaseModel):
        a = 'foobar'

        class Config:
            fields = {'a': '_a'}

    assert Model(_a='different').a == 'different'
    assert repr(Model.__fields__['a']) == (
        "ModelField(name='a', type=str, required=False, default='foobar', alias='_a')"
    )


def test_alias_error():
    class Model(BaseModel):
        a = 123

        class Config:
            fields = {'a': '_a'}

    assert Model(_a='123').a == 123

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


def test_annotation_config():
    class Model(BaseModel):
        b: float
        a: int = 10
        _c: str

        class Config:
            fields = {'b': 'foobar'}

    assert list(Model.__fields__.keys()) == ['b', 'a']
    assert [f.alias for f in Model.__fields__.values()] == ['foobar', 'a']
    assert Model(foobar='123').b == 123.0


def test_alias_camel_case():
    class Model(BaseModel):
        one_thing: int
        another_thing: int

        class Config(BaseConfig):
            @classmethod
            def get_field_info(cls, name):
                field_config = super().get_field_info(name) or {}
                if 'alias' not in field_config:
                    field_config['alias'] = re.sub(r'(?:^|_)([a-z])', lambda m: m.group(1).upper(), name)
                return field_config

    v = Model(**{'OneThing': 123, 'AnotherThing': '321'})
    assert v.one_thing == 123
    assert v.another_thing == 321
    assert v == {'one_thing': 123, 'another_thing': 321}


def test_get_field_info_inherit():
    class ModelOne(BaseModel):
        class Config(BaseConfig):
            @classmethod
            def get_field_info(cls, name):
                field_config = super().get_field_info(name) or {}
                if 'alias' not in field_config:
                    field_config['alias'] = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
                return field_config

    class ModelTwo(ModelOne):
        one_thing: int
        another_thing: int
        third_thing: int

        class Config:
            fields = {'third_thing': 'Banana'}

    v = ModelTwo(**{'oneThing': 123, 'anotherThing': '321', 'Banana': 1})
    assert v == {'one_thing': 123, 'another_thing': 321, 'third_thing': 1}


def test_pop_by_field_name():
    class Model(BaseModel):
        last_updated_by: Optional[str] = None

        class Config:
            extra = Extra.forbid
            allow_population_by_field_name = True
            fields = {'last_updated_by': 'lastUpdatedBy'}

    assert Model(lastUpdatedBy='foo').dict() == {'last_updated_by': 'foo'}
    assert Model(last_updated_by='foo').dict() == {'last_updated_by': 'foo'}
    with pytest.raises(ValidationError) as exc_info:
        Model(lastUpdatedBy='foo', last_updated_by='bar')
    assert exc_info.value.errors() == [
        {'loc': ('last_updated_by',), 'msg': 'extra fields not permitted', 'type': 'value_error.extra'}
    ]


def test_alias_child_precedence():
    class Parent(BaseModel):
        x: int

        class Config:
            fields = {'x': 'x1'}

    class Child(Parent):
        y: int

        class Config:
            fields = {'y': 'y2', 'x': 'x2'}

    assert Child.__fields__['y'].alias == 'y2'
    assert Child.__fields__['x'].alias == 'x2'


def test_alias_generator_parent():
    class Parent(BaseModel):
        x: int

        class Config:
            allow_population_by_field_name = True

            @classmethod
            def alias_generator(cls, f_name):
                return f_name + '1'

    class Child(Parent):
        y: int

        class Config:
            @classmethod
            def alias_generator(cls, f_name):
                return f_name + '2'

    assert Child.__fields__['y'].alias == 'y2'
    assert Child.__fields__['x'].alias == 'x2'


def test_alias_generator_on_parent():
    class Parent(BaseModel):
        x: bool = Field(..., alias='a_b_c')
        y: str

        class Config:
            @staticmethod
            def alias_generator(x):
                return x.upper()

    class Child(Parent):
        y: str
        z: str

    assert Parent.__fields__['x'].alias == 'a_b_c'
    assert Parent.__fields__['y'].alias == 'Y'
    assert Child.__fields__['x'].alias == 'a_b_c'
    assert Child.__fields__['y'].alias == 'Y'
    assert Child.__fields__['z'].alias == 'Z'


def test_alias_generator_on_child():
    class Parent(BaseModel):
        x: bool = Field(..., alias='abc')
        y: str

    class Child(Parent):
        y: str
        z: str

        class Config:
            @staticmethod
            def alias_generator(x):
                return x.upper()

    assert [f.alias for f in Parent.__fields__.values()] == ['abc', 'y']
    assert [f.alias for f in Child.__fields__.values()] == ['abc', 'Y', 'Z']


def test_low_priority_alias():
    class Parent(BaseModel):
        x: bool = Field(..., alias='abc', alias_priority=1)
        y: str

    class Child(Parent):
        y: str
        z: str

        class Config:
            @staticmethod
            def alias_generator(x):
                return x.upper()

    assert [f.alias for f in Parent.__fields__.values()] == ['abc', 'y']
    assert [f.alias for f in Child.__fields__.values()] == ['X', 'Y', 'Z']


def test_low_priority_alias_config():
    class Parent(BaseModel):
        x: bool
        y: str

        class Config:
            fields = {'x': dict(alias='abc', alias_priority=1)}

    class Child(Parent):
        y: str
        z: str

        class Config:
            @staticmethod
            def alias_generator(x):
                return x.upper()

    assert [f.alias for f in Parent.__fields__.values()] == ['abc', 'y']
    assert [f.alias for f in Child.__fields__.values()] == ['X', 'Y', 'Z']


def test_field_vs_config():
    class Model(BaseModel):
        x: str = Field(..., alias='x_on_field')
        y: str
        z: str

        class Config:
            fields = {'x': dict(alias='x_on_config'), 'y': dict(alias='y_on_config')}

    assert [f.alias for f in Model.__fields__.values()] == ['x_on_field', 'y_on_config', 'z']


def test_alias_priority():
    class Parent(BaseModel):
        a: str = Field(..., alias='a_field_parent')
        b: str = Field(..., alias='b_field_parent')
        c: str = Field(..., alias='c_field_parent')
        d: str
        e: str

        class Config:
            fields = {
                'a': dict(alias='a_config_parent'),
                'c': dict(alias='c_config_parent'),
                'd': dict(alias='d_config_parent'),
            }

            @staticmethod
            def alias_generator(x):
                return f'{x}_generator_parent'

    class Child(Parent):
        a: str = Field(..., alias='a_field_child')

        class Config:
            fields = {'a': dict(alias='a_config_child'), 'b': dict(alias='b_config_child')}

            @staticmethod
            def alias_generator(x):
                return f'{x}_generator_child'

    # debug([f.alias for f in Parent.__fields__.values()], [f.alias for f in Child.__fields__.values()])
    assert [f.alias for f in Parent.__fields__.values()] == [
        'a_field_parent',
        'b_field_parent',
        'c_field_parent',
        'd_config_parent',
        'e_generator_parent',
    ]
    assert [f.alias for f in Child.__fields__.values()] == [
        'a_field_child',
        'b_config_child',
        'c_field_parent',
        'd_config_parent',
        'e_generator_child',
    ]
