from typing import Any

import pytest

from rendercv.exception import RenderCVUserError
from rendercv.schema.override_dictionary import (
    apply_overrides_to_dictionary,
    update_value_by_location,
)


class TestUpdateValueByLocation:
    @pytest.mark.parametrize(
        ("initial_dict", "key", "value", "expected"),
        [
            ({"name": "John"}, "name", "Jane", {"name": "Jane"}),
            ({"age": 30}, "age", "25", {"age": "25"}),
            ({}, "new_key", "new_value", {"new_key": "new_value"}),
        ],
    )
    def test_simple_dictionary_updates(self, initial_dict, key, value, expected):
        result = update_value_by_location(initial_dict, key, value, key)
        assert result == expected

    @pytest.mark.parametrize(
        ("initial_dict", "key", "value", "expected"),
        [
            (
                {"user": {"name": "John"}},
                "user.name",
                "Jane",
                {"user": {"name": "Jane"}},
            ),
            (
                {"cv": {"sections": {"education": "MIT"}}},
                "cv.sections.education",
                "Harvard",
                {"cv": {"sections": {"education": "Harvard"}}},
            ),
            (
                {"a": {"b": {"c": {"d": "old"}}}},
                "a.b.c.d",
                "new",
                {"a": {"b": {"c": {"d": "new"}}}},
            ),
        ],
    )
    def test_nested_dictionary_updates(self, initial_dict, key, value, expected):
        result = update_value_by_location(initial_dict, key, value, key)
        assert result == expected

    @pytest.mark.parametrize(
        ("initial_dict", "key", "value", "expected"),
        [
            (
                {},
                "design.theme",
                "moderncv",
                {"design": {"theme": "moderncv"}},
            ),
            (
                {"cv": {"name": "John"}},
                "design.theme",
                "classic",
                {"cv": {"name": "John"}, "design": {"theme": "classic"}},
            ),
            (
                {"cv": {}},
                "cv.sections.education",
                "MIT",
                {"cv": {"sections": {"education": "MIT"}}},
            ),
        ],
    )
    def test_missing_intermediate_keys_are_created(
        self, initial_dict, key, value, expected
    ):
        result = update_value_by_location(initial_dict, key, value, key)
        assert result == expected

    @pytest.mark.parametrize(
        ("initial_list", "key", "value", "expected"),
        [
            (["a", "b", "c"], "0", "x", ["x", "b", "c"]),
            (["a", "b", "c"], "1", "y", ["a", "y", "c"]),
            (["a", "b", "c"], "2", "z", ["a", "b", "z"]),
        ],
    )
    def test_list_updates(self, initial_list, key, value, expected):
        result = update_value_by_location(initial_list, key, value, f"list.{key}")
        assert result == expected

    @pytest.mark.parametrize(
        ("initial_dict", "key", "value", "expected"),
        [
            (
                {"items": ["a", "b", "c"]},
                "items.1",
                "updated",
                {"items": ["a", "updated", "c"]},
            ),
            (
                {"cv": {"sections": ["edu", "exp"]}},
                "cv.sections.0",
                "education",
                {"cv": {"sections": ["education", "exp"]}},
            ),
            (
                {"data": [{"name": "John"}, {"name": "Jane"}]},
                "data.0.name",
                "Bob",
                {"data": [{"name": "Bob"}, {"name": "Jane"}]},
            ),
            (
                {"cv": {"sections": {"education": [{"institution": "MIT"}]}}},
                "cv.sections.education.0.institution",
                "Harvard",
                {"cv": {"sections": {"education": [{"institution": "Harvard"}]}}},
            ),
        ],
    )
    def test_mixed_dict_list_traversal(self, initial_dict, key, value, expected):
        result = update_value_by_location(initial_dict, key, value, key)
        assert result == expected

    @pytest.mark.parametrize(
        ("initial_dict", "key", "full_key"),
        [
            ({"items": ["a", "b"]}, "items.invalid", "items.invalid"),
            ({"data": [1, 2, 3]}, "data.foo", "data.foo"),
        ],
    )
    def test_non_integer_index_for_list_raises_error(self, initial_dict, key, full_key):
        with pytest.raises(
            RenderCVUserError, match=r"corresponds to a list, but .* is not an integer"
        ):
            update_value_by_location(initial_dict, key, "value", full_key)

    @pytest.mark.parametrize(
        ("initial_dict", "key", "full_key"),
        [
            ({"items": ["a", "b"]}, "items.5", "items.5"),
            ({"data": [1]}, "data.10", "data.10"),
        ],
    )
    def test_index_out_of_range_raises_error(self, initial_dict, key, full_key):
        with pytest.raises(RenderCVUserError, match=r"Index .* is out of range"):
            update_value_by_location(initial_dict, key, "value", full_key)

    @pytest.mark.parametrize(
        ("initial_dict", "key", "full_key"),
        [
            ({"name": "John"}, "name.field", "name.field"),
            ({"age": 30}, "age.value", "age.value"),
        ],
    )
    def test_invalid_structure_raises_error(self, initial_dict, key, full_key):
        with pytest.raises(
            RenderCVUserError, match="It seems like there's something wrong"
        ):
            update_value_by_location(initial_dict, key, "value", full_key)

    def test_mutates_original_structure(self):
        original = {"name": "John"}
        result = update_value_by_location(original, "name", "Jane", "name")
        assert result is original
        assert original == {"name": "Jane"}

    def test_deeply_nested_structure(self):
        initial: dict[str, Any] = {
            "cv": {
                "sections": {
                    "education": [
                        {
                            "institution": "MIT",
                            "degree": "PhD",
                            "details": {"gpa": "4.0"},
                        }
                    ]
                }
            }
        }
        result = update_value_by_location(
            initial,
            "cv.sections.education.0.details.gpa",
            "3.9",
            "cv.sections.education.0.details.gpa",
        )
        assert result["cv"]["sections"]["education"][0]["details"]["gpa"] == "3.9"


class TestApplyOverridesToDictionary:
    @pytest.mark.parametrize(
        ("initial_dict", "overrides", "expected"),
        [
            ({"name": "John"}, {"name": "Jane"}, {"name": "Jane"}),
            (
                {"cv": {"name": "John"}},
                {"cv.name": "Jane"},
                {"cv": {"name": "Jane"}},
            ),
            (
                {"items": ["a", "b"]},
                {"items.0": "x"},
                {"items": ["x", "b"]},
            ),
        ],
    )
    def test_single_override(self, initial_dict, overrides, expected):
        result = apply_overrides_to_dictionary(initial_dict, overrides)
        assert result == expected

    @pytest.mark.parametrize(
        ("initial_dict", "overrides", "expected"),
        [
            (
                {"name": "John", "age": "30"},
                {"name": "Jane", "age": "25"},
                {"name": "Jane", "age": "25"},
            ),
            (
                {"cv": {"name": "John", "email": "john@example.com"}},
                {"cv.name": "Jane", "cv.email": "jane@example.com"},
                {"cv": {"name": "Jane", "email": "jane@example.com"}},
            ),
            (
                {"cv": {"sections": {"education": "MIT", "experience": "Google"}}},
                {"cv.sections.education": "Harvard", "cv.sections.experience": "Meta"},
                {"cv": {"sections": {"education": "Harvard", "experience": "Meta"}}},
            ),
        ],
    )
    def test_multiple_overrides(self, initial_dict, overrides, expected):
        result = apply_overrides_to_dictionary(initial_dict, overrides)
        assert result == expected

    def test_does_not_mutate_original(self):
        original = {"name": "John", "age": "30"}
        original_copy = {"name": "John", "age": "30"}
        overrides = {"name": "Jane"}

        result = apply_overrides_to_dictionary(original, overrides)

        assert original == original_copy  # Original unchanged
        assert result == {"name": "Jane", "age": "30"}  # Result has override
        assert result is not original  # Different objects

    def test_deep_copy_behavior(self):
        original = {"cv": {"sections": {"education": ["MIT"]}}}
        overrides = {"cv.sections.education.0": "Harvard"}

        result = apply_overrides_to_dictionary(original, overrides)

        assert original["cv"]["sections"]["education"][0] == "MIT"
        assert result["cv"]["sections"]["education"][0] == "Harvard"

    def test_empty_overrides(self):
        original = {"name": "John", "age": "30"}
        result = apply_overrides_to_dictionary(original, {})
        assert result == original
        assert result is not original

    def test_complex_cv_scenario(self):
        initial: dict[str, Any] = {
            "cv": {
                "name": "John Doe",
                "sections": {
                    "education": [
                        {
                            "institution": "MIT",
                            "degree": "PhD",
                            "start_date": "2020-01",
                        }
                    ],
                    "experience": [{"company": "Google", "title": "Engineer"}],
                },
            }
        }
        overrides = {
            "cv.name": "Jane Doe",
            "cv.sections.education.0.institution": "Harvard",
            "cv.sections.education.0.start_date": "2021-01",
            "cv.sections.experience.0.company": "Meta",
        }

        result = apply_overrides_to_dictionary(initial, overrides)

        assert result["cv"]["name"] == "Jane Doe"
        assert result["cv"]["sections"]["education"][0]["institution"] == "Harvard"
        assert result["cv"]["sections"]["education"][0]["start_date"] == "2021-01"
        assert result["cv"]["sections"]["education"][0]["degree"] == "PhD"
        assert result["cv"]["sections"]["experience"][0]["company"] == "Meta"
        assert result["cv"]["sections"]["experience"][0]["title"] == "Engineer"
        assert initial["cv"]["name"] == "John Doe"
