import inspect
from datetime import date, datetime
from enum import Enum
from typing import Dict, List, Optional, TypeVar, Union
from unittest import TestCase

from edpopenapibuilder.utils import (
    build_ref,
    generate_uuid,
    get_dict_path_value,
    get_inner_optional_type,
    get_type_name,
    get_type_params,
    is_dict,
    is_enum,
    is_list,
    is_optional,
    is_scalar,
    is_type,
    is_typevar,
    is_typing_alias,
    is_union,
    set_dict_path_value,
    to_camel_case,
    value_hasher,
    _build_salt,
)


class _SampleEnum(Enum):
    A = "a"
    B = "b"


class _SampleClass:
    pass


T = TypeVar("T")


# --- set_dict_path_value ---

class TestSetDictPathValue(TestCase):
    def test_string_path(self):
        d = {}
        set_dict_path_value(d, "key", 42)
        self.assertEqual(d, {"key": 42})

    def test_list_path_single(self):
        d = {}
        set_dict_path_value(d, ["key"], "val")
        self.assertEqual(d, {"key": "val"})

    def test_list_path_nested(self):
        d = {}
        set_dict_path_value(d, ["a", "b", "c"], 1)
        self.assertEqual(d, {"a": {"b": {"c": 1}}})

    def test_overwrites_existing(self):
        d = {"key": "old"}
        set_dict_path_value(d, "key", "new")
        self.assertEqual(d["key"], "new")

    def test_creates_intermediate_dicts(self):
        d = {}
        set_dict_path_value(d, ["x", "y"], 10)
        self.assertIsInstance(d["x"], dict)
        self.assertEqual(d["x"]["y"], 10)

    def test_preserves_existing_intermediate(self):
        d = {"a": {"existing": True}}
        set_dict_path_value(d, ["a", "new_key"], 5)
        self.assertTrue(d["a"]["existing"])
        self.assertEqual(d["a"]["new_key"], 5)


# --- get_dict_path_value ---

class TestGetDictPathValue(TestCase):
    def test_string_path(self):
        self.assertEqual(get_dict_path_value({"k": 1}, "k"), 1)

    def test_list_path(self):
        self.assertEqual(get_dict_path_value({"a": {"b": 2}}, ["a", "b"]), 2)

    def test_missing_key_returns_default(self):
        self.assertIsNone(get_dict_path_value({}, "missing"))

    def test_missing_key_returns_custom_default(self):
        self.assertEqual(get_dict_path_value({}, "missing", default_value="nope"), "nope")

    def test_missing_nested_key_returns_default(self):
        self.assertIsNone(get_dict_path_value({"a": {}}, ["a", "b"]))

    def test_none_value_is_returned(self):
        self.assertIsNone(get_dict_path_value({"k": None}, "k"))

    def test_none_value_not_replaced_by_default(self):
        result = get_dict_path_value({"k": None}, "k", default_value="fallback")
        self.assertIsNone(result)

    def test_false_value_is_returned(self):
        self.assertFalse(get_dict_path_value({"k": False}, "k"))

    def test_zero_value_is_returned(self):
        self.assertEqual(get_dict_path_value({"k": 0}, "k", default_value=99), 0)

    def test_empty_path_returns_default(self):
        self.assertIsNone(get_dict_path_value({"k": 1}, []))


# --- build_ref ---

class TestBuildRef(TestCase):
    def test_string_path(self):
        self.assertEqual(build_ref("components/schemas", "Foo"), "#/components/schemas/Foo")

    def test_list_path(self):
        self.assertEqual(build_ref(["components", "schemas"], "Bar"), "#/components/schemas/Bar")


# --- Type introspection helpers ---

class TestIsOptional(TestCase):
    def test_optional_str(self):
        self.assertTrue(is_optional(Optional[str]))

    def test_optional_int(self):
        self.assertTrue(is_optional(Optional[int]))

    def test_plain_str(self):
        self.assertFalse(is_optional(str))

    def test_plain_int(self):
        self.assertFalse(is_optional(int))

    def test_union_two_types(self):
        self.assertFalse(is_optional(Union[str, int]))

    def test_list_not_optional(self):
        self.assertFalse(is_optional(List[str]))


class TestIsTypingAlias(TestCase):
    def test_list_alias(self):
        self.assertTrue(is_typing_alias(List[str]))

    def test_dict_alias(self):
        self.assertTrue(is_typing_alias(Dict[str, int]))

    def test_optional_alias(self):
        self.assertTrue(is_typing_alias(Optional[str]))

    def test_plain_type(self):
        self.assertFalse(is_typing_alias(str))

    def test_class(self):
        self.assertFalse(is_typing_alias(_SampleClass))


class TestIsList(TestCase):
    def test_bare_list(self):
        self.assertTrue(is_list(list))

    def test_typing_list(self):
        self.assertTrue(is_list(List[str]))

    def test_typing_list_bare(self):
        self.assertTrue(is_list(List))

    def test_dict_not_list(self):
        self.assertFalse(is_list(dict))

    def test_str_not_list(self):
        self.assertFalse(is_list(str))


class TestIsDict(TestCase):
    def test_bare_dict(self):
        self.assertTrue(is_dict(dict))

    def test_typing_dict(self):
        self.assertTrue(is_dict(Dict[str, int]))

    def test_list_not_dict(self):
        self.assertFalse(is_dict(list))


class TestIsUnion(TestCase):
    def test_union(self):
        self.assertTrue(is_union(Union[str, int]))

    def test_optional_is_union(self):
        self.assertTrue(is_union(Optional[str]))

    def test_plain_type(self):
        self.assertFalse(is_union(str))


class TestIsType(TestCase):
    def test_class(self):
        self.assertTrue(is_type(_SampleClass))

    def test_builtin(self):
        self.assertTrue(is_type(str))

    def test_list_alias(self):
        self.assertTrue(is_type(List[str]))

    def test_none(self):
        self.assertFalse(is_type(None))


class TestIsEnum(TestCase):
    def test_enum_class(self):
        self.assertTrue(is_enum(_SampleEnum))

    def test_plain_class(self):
        self.assertFalse(is_enum(_SampleClass))

    def test_str(self):
        self.assertFalse(is_enum(str))

    def test_enum_instance(self):
        self.assertFalse(is_enum(_SampleEnum.A))


class TestIsTypevar(TestCase):
    def test_typevar(self):
        self.assertTrue(is_typevar(T))

    def test_class(self):
        self.assertFalse(is_typevar(str))


class TestGetTypeParams(TestCase):
    def test_list_str(self):
        self.assertEqual(get_type_params(List[str]), [str])

    def test_dict_str_int(self):
        self.assertEqual(get_type_params(Dict[str, int]), [str, int])

    def test_optional_str(self):
        params = get_type_params(Optional[str])
        self.assertIn(str, params)

    def test_plain_type_empty(self):
        self.assertEqual(get_type_params(str), [])

    def test_filters_typevars(self):
        self.assertNotIn(T, get_type_params(List[str]))


class TestGetInnerOptionalType(TestCase):
    def test_optional_str(self):
        self.assertEqual(get_inner_optional_type(Optional[str]), str)

    def test_optional_int(self):
        self.assertEqual(get_inner_optional_type(Optional[int]), int)

    def test_plain_type_returns_none(self):
        self.assertIsNone(get_inner_optional_type(str))


class TestIsScalar(TestCase):
    def test_bool(self):
        self.assertTrue(is_scalar(bool))

    def test_int(self):
        self.assertTrue(is_scalar(int))

    def test_float(self):
        self.assertTrue(is_scalar(float))

    def test_complex(self):
        self.assertTrue(is_scalar(complex))

    def test_str(self):
        self.assertTrue(is_scalar(str))

    def test_empty_signature(self):
        self.assertTrue(is_scalar(inspect.Signature.empty))

    def test_list_not_scalar(self):
        self.assertFalse(is_scalar(list))

    def test_class_not_scalar(self):
        self.assertFalse(is_scalar(_SampleClass))

    def test_date_not_scalar(self):
        self.assertFalse(is_scalar(date))


# --- Hashing and naming ---

class TestValueHasher(TestCase):
    def test_returns_10_chars(self):
        self.assertEqual(len(value_hasher("test")), 10)

    def test_deterministic(self):
        self.assertEqual(value_hasher("hello"), value_hasher("hello"))

    def test_different_inputs_differ(self):
        self.assertNotEqual(value_hasher("a"), value_hasher("b"))

    def test_hex_characters(self):
        result = value_hasher("test")
        self.assertRegex(result, r'^[0-9a-f]{10}$')


class TestToCamelCase(TestCase):
    def test_snake_case(self):
        self.assertEqual(to_camel_case("hello_world"), "helloWorld")

    def test_single_word(self):
        self.assertEqual(to_camel_case("hello"), "hello")

    def test_multiple_underscores(self):
        self.assertEqual(to_camel_case("one_two_three"), "oneTwoThree")

    def test_hyphenated(self):
        self.assertEqual(to_camel_case("hello-world"), "helloWorld")

    def test_mixed_hyphens_underscores(self):
        self.assertEqual(to_camel_case("a-b_c"), "aBC")

    def test_already_camel(self):
        self.assertEqual(to_camel_case("alreadyCamel"), "alreadycamel")


# --- _build_salt ---

class TestBuildSalt(TestCase):
    def test_none(self):
        self.assertEqual(_build_salt(None), "")

    def test_empty_string(self):
        self.assertEqual(_build_salt(""), "")

    def test_string_not_split(self):
        self.assertEqual(_build_salt("abc"), "abc")

    def test_list(self):
        self.assertEqual(_build_salt(["a", "b"]), "a,b")

    def test_tuple(self):
        self.assertEqual(_build_salt(("x", "y")), "x,y")

    def test_set_sorted(self):
        result = _build_salt({"b", "a", "c"})
        self.assertEqual(result, "a,b,c")

    def test_set_deterministic(self):
        self.assertEqual(_build_salt({"z", "a", "m"}), _build_salt({"m", "z", "a"}))

    def test_empty_set(self):
        self.assertEqual(_build_salt(set()), "")

    def test_int_fallback(self):
        self.assertEqual(_build_salt(42), "42")


# --- generate_uuid and get_type_name ---

class TestGetTypeName(TestCase):
    def test_class(self):
        self.assertEqual(get_type_name(_SampleClass), "_SampleClass")

    def test_builtin(self):
        self.assertEqual(get_type_name(str), "str")

    def test_no_name_attr(self):
        result = get_type_name(List[str])
        self.assertIsInstance(result, str)


class TestGenerateUuid(TestCase):
    def test_deterministic(self):
        self.assertEqual(generate_uuid(str), generate_uuid(str))

    def test_different_types_differ(self):
        self.assertNotEqual(generate_uuid(str), generate_uuid(int))

    def test_salt_changes_uuid(self):
        self.assertNotEqual(generate_uuid(str), generate_uuid(str, salt="x"))

    def test_different_salts_differ(self):
        self.assertNotEqual(generate_uuid(str, salt="a"), generate_uuid(str, salt="b"))

    def test_set_salt_deterministic(self):
        self.assertEqual(
            generate_uuid(str, salt={"b", "a"}),
            generate_uuid(str, salt={"a", "b"}),
        )

    def test_returns_10_char_hex(self):
        self.assertRegex(generate_uuid(str), r'^[0-9a-f]{10}$')