"""Tests for kokoro_chat.character (model + loader)."""
from __future__ import annotations

import json
from types import SimpleNamespace

import pytest

from kokoro_chat.character.model import (
    Character,
    _parse_json_dict,
    _parse_json_list,
    _parse_json_list_of_dicts,
    _parse_secrets,
)
from kokoro_chat.character.loader import map_v2_yaml_to_db


# ===========================================================================
# JSON helper tests
# ===========================================================================


class TestParseJsonList:
    """Tests for _parse_json_list."""

    @pytest.mark.parametrize(
        "raw, expected",
        [
            (None, []),
            ("", []),
            ('["a", "b"]', ["a", "b"]),
            ("[]", []),
            ('{"key": "val"}', []),       # dict when expecting list
            ("not json", []),             # invalid JSON
            ("123", []),                  # number, not list
            ('"just a string"', []),      # JSON string, not list
        ],
        ids=[
            "none",
            "empty-string",
            "valid-array",
            "empty-array",
            "dict-returns-empty",
            "invalid-json",
            "number-returns-empty",
            "json-string-returns-empty",
        ],
    )
    def test_cases(self, raw, expected):
        assert _parse_json_list(raw) == expected


class TestParseJsonListOfDicts:
    """Tests for _parse_json_list_of_dicts."""

    @pytest.mark.parametrize(
        "raw, expected",
        [
            (None, []),
            ("", []),
            ('[{"user": "hi", "char": "hello"}]', [{"user": "hi", "char": "hello"}]),
            ("[]", []),
            ('{"key": "val"}', []),
            ("bad json!", []),
        ],
        ids=[
            "none",
            "empty-string",
            "valid-list-of-dicts",
            "empty-array",
            "dict-returns-empty",
            "invalid-json",
        ],
    )
    def test_cases(self, raw, expected):
        assert _parse_json_list_of_dicts(raw) == expected


class TestParseJsonDict:
    """Tests for _parse_json_dict."""

    @pytest.mark.parametrize(
        "raw, expected",
        [
            (None, {}),
            ("", {}),
            ('{"a": 1}', {"a": 1}),
            ("{}", {}),
            ('["not", "a", "dict"]', {}),  # list when expecting dict
            ("nope", {}),                   # invalid JSON
            ("42", {}),                     # number, not dict
        ],
        ids=[
            "none",
            "empty-string",
            "valid-dict",
            "empty-dict",
            "list-returns-empty",
            "invalid-json",
            "number-returns-empty",
        ],
    )
    def test_cases(self, raw, expected):
        assert _parse_json_dict(raw) == expected


class TestParseSecrets:
    """Tests for _parse_secrets."""

    @pytest.mark.parametrize(
        "raw, expected",
        [
            (None, []),
            ("", []),
            ("not json", []),
            ('{"not": "a list"}', []),
        ],
        ids=["none", "empty-string", "invalid-json", "dict-returns-empty"],
    )
    def test_edge_cases(self, raw, expected):
        assert _parse_secrets(raw) == expected

    def test_valid_secrets(self):
        secrets = [
            {
                "content": "I secretly love cats",
                "reveal_condition": {"min_affection": 60, "min_trust": 50},
            },
            {
                "content": "I used to be a spy",
                "reveal_condition": {"min_affection": 80},
            },
        ]
        raw = json.dumps(secrets)
        result = _parse_secrets(raw)
        assert len(result) == 2
        assert result[0]["content"] == "I secretly love cats"
        assert result[1]["reveal_condition"]["min_affection"] == 80


# ===========================================================================
# Character.from_db_model tests
# ===========================================================================


def _make_db_char(**overrides) -> SimpleNamespace:
    """Build a SimpleNamespace mimicking a DB Character row."""
    defaults = {
        "id": "char-001",
        "name": "Sakura",
        "personality": "cheerful and kind",
        "background": "grew up in a small town",
        "speaking_style": "uses casual speech",
        "mbti": "ENFP",
        "greeting": "Hi there!",
        "appearance": "long pink hair",
        "likes": '["flowers", "tea"]',
        "dislikes": '["spiders"]',
        "fears": '["thunder"]',
        "quirks": '["hums when nervous"]',
        "speech_patterns": "often ends sentences with ~",
        "secrets_json": json.dumps([{"content": "secret1", "reveal_condition": {}}]),
        "default_mood": "happy",
        "mes_example": json.dumps([{"user": "hello", "char": "hey!"}]),
        "post_history_instructions": "stay in character",
        "description": "A cheerful girl",
        "personality_summary": "ENFP type personality",
        "scenario": "coffee shop",
        "system_prompt": "You are Sakura",
        "alternate_greetings": '["Yo!", "Hey~"]',
        "character_book_json": json.dumps({"entries": [{"keyword": "flowers"}]}),
        "extensions_json": json.dumps({
            "kokoro": {
                "speech_patterns": {"default": "casual", "happy": "excited!"},
                "affection_overrides": {"lover": "very sweet"},
                "post_history_by_level": {"stranger": "be polite"},
                "anti_patterns": [{"bad": "...", "correct": "~", "why": "style"}],
            }
        }),
    }
    defaults.update(overrides)
    return SimpleNamespace(**defaults)


class TestCharacterFromDbModel:
    """Tests for Character.from_db_model."""

    def test_full_db_model(self):
        db = _make_db_char()
        char = Character.from_db_model(db)

        assert char.id == "char-001"
        assert char.name == "Sakura"
        assert char.personality == "cheerful and kind"
        assert char.background == "grew up in a small town"
        assert char.speaking_style == "uses casual speech"
        assert char.mbti == "ENFP"
        assert char.greeting == "Hi there!"
        assert char.appearance == "long pink hair"
        assert char.likes == ["flowers", "tea"]
        assert char.dislikes == ["spiders"]
        assert char.fears == ["thunder"]
        assert char.quirks == ["hums when nervous"]
        assert char.speech_patterns == "often ends sentences with ~"
        assert len(char.secrets) == 1
        assert char.secrets[0]["content"] == "secret1"
        assert char.default_mood == "happy"
        assert char.mes_example == [{"user": "hello", "char": "hey!"}]
        assert char.post_history_instructions == "stay in character"
        assert char.description == "A cheerful girl"
        assert char.personality_summary == "ENFP type personality"
        assert char.scenario == "coffee shop"
        assert char.system_prompt == "You are Sakura"
        assert char.alternate_greetings == ["Yo!", "Hey~"]
        assert char.character_book == {"entries": [{"keyword": "flowers"}]}

    def test_kokoro_extensions_extracted(self):
        db = _make_db_char()
        char = Character.from_db_model(db)

        assert char.speech_patterns_v2 == {
            "default": "casual",
            "happy": "excited!",
        }
        assert char.affection_overrides == {"lover": "very sweet"}
        assert char.post_history_by_level == {"stranger": "be polite"}
        assert len(char.anti_patterns) == 1
        assert char.anti_patterns[0]["bad"] == "..."

    def test_missing_optional_fields_use_defaults(self):
        """DB model without V2/kokoro optional attrs should fall back."""
        db = SimpleNamespace(
            id=42,
            name="Minimal",
            personality=None,
            background=None,
            speaking_style=None,
            mbti=None,
            greeting=None,
            appearance=None,
            likes=None,
            dislikes=None,
            fears=None,
            quirks=None,
            speech_patterns=None,
            secrets_json=None,
            default_mood=None,
            mes_example=None,
            post_history_instructions=None,
            # Deliberately omit: description, personality_summary, scenario,
            # system_prompt, alternate_greetings, character_book_json,
            # extensions_json  -> getattr returns default
        )
        char = Character.from_db_model(db)

        assert char.id == "42"
        assert char.name == "Minimal"
        assert char.personality == ""
        assert char.likes == []
        assert char.secrets == []
        assert char.default_mood == "neutral"
        assert char.description == ""
        assert char.personality_summary == ""
        assert char.scenario == ""
        assert char.system_prompt == ""
        assert char.alternate_greetings == []
        assert char.character_book == {}
        assert char.speech_patterns_v2 == {}
        assert char.affection_overrides == {}
        assert char.post_history_by_level == {}
        assert char.anti_patterns == []

    def test_invalid_extensions_json_graceful_fallback(self):
        db = _make_db_char(extensions_json="not valid json")
        char = Character.from_db_model(db)

        assert char.speech_patterns_v2 == {}
        assert char.affection_overrides == {}
        assert char.post_history_by_level == {}
        assert char.anti_patterns == []

    def test_extensions_json_not_dict_graceful_fallback(self):
        db = _make_db_char(extensions_json='["a list instead"]')
        char = Character.from_db_model(db)

        assert char.speech_patterns_v2 == {}
        assert char.affection_overrides == {}

    def test_invalid_character_book_json_returns_empty(self):
        db = _make_db_char(character_book_json="broken")
        char = Character.from_db_model(db)
        assert char.character_book == {}

    def test_id_converted_to_string(self):
        db = _make_db_char(id=999)
        char = Character.from_db_model(db)
        assert char.id == "999"
        assert isinstance(char.id, str)


# ===========================================================================
# map_v2_yaml_to_db tests
# ===========================================================================


class TestMapV2YamlToDb:
    """Tests for map_v2_yaml_to_db."""

    def test_minimal_yaml_name_only(self):
        result = map_v2_yaml_to_db({"name": "Test"})

        assert result["name"] == "Test"
        assert result["description"] == ""
        assert result["greeting"] == ""
        assert result["mbti"] == ""
        assert result["default_mood"] == "neutral"
        assert result["speech_patterns"] == ""
        assert result["personality"] == ""
        assert result["background"] == ""
        assert result["speaking_style"] == ""
        assert result["likes"] is None
        assert result["dislikes"] is None

    def test_full_v2_yaml(self):
        data = {
            "name": "Sakura",
            "description": "A cheerful girl",
            "personality": "kind and sweet",
            "scenario": "at school",
            "first_mes": "Hello!",
            "system_prompt": "You are Sakura.",
            "post_history_instructions": "stay IC",
            "creator": "wade",
            "character_version": "1.0",
            "alternate_greetings": ["Hi!", "Hey~"],
            "mes_example": [{"user": "yo", "char": "hey"}],
            "character_book": {"entries": [{"keyword": "test"}]},
            "extensions": {
                "kokoro": {
                    "mbti": "ENFP",
                    "default_mood": "happy",
                    "likes": ["flowers"],
                    "dislikes": ["bugs"],
                    "fears": ["dark"],
                    "quirks": ["hums"],
                    "secrets": [{"content": "shh"}],
                    "speech_patterns": "speaks softly",
                    "affection_overrides": {"lover": "clingy"},
                    "post_history_by_level": {"stranger": "polite"},
                    "anti_patterns": [{"bad": "x", "correct": "y", "why": "z"}],
                },
            },
        }
        result = map_v2_yaml_to_db(data)

        assert result["name"] == "Sakura"
        assert result["description"] == "A cheerful girl"
        assert result["personality_summary"] == "kind and sweet"
        assert result["scenario"] == "at school"
        assert result["greeting"] == "Hello!"
        assert result["system_prompt"] == "You are Sakura."
        assert result["post_history_instructions"] == "stay IC"
        assert result["creator"] == "wade"
        assert result["character_version"] == "1.0"
        assert result["mbti"] == "ENFP"
        assert result["default_mood"] == "happy"
        assert result["personality"] == "A cheerful girl"  # fallback to description

        # JSON-encoded array fields
        assert json.loads(result["alternate_greetings"]) == ["Hi!", "Hey~"]
        assert json.loads(result["mes_example"]) == [{"user": "yo", "char": "hey"}]
        assert json.loads(result["character_book_json"]) == {
            "entries": [{"keyword": "test"}],
        }
        assert json.loads(result["likes"]) == ["flowers"]
        assert json.loads(result["dislikes"]) == ["bugs"]
        assert json.loads(result["fears"]) == ["dark"]
        assert json.loads(result["quirks"]) == ["hums"]
        assert json.loads(result["secrets_json"]) == [{"content": "shh"}]

    def test_speech_patterns_dict_extracts_default(self):
        data = {
            "name": "SP",
            "extensions": {
                "kokoro": {
                    "speech_patterns": {
                        "default": "calm tone",
                        "angry": "sharp words",
                    },
                },
            },
        }
        result = map_v2_yaml_to_db(data)
        assert result["speech_patterns"] == "calm tone"

    def test_speech_patterns_dict_missing_default_key(self):
        data = {
            "name": "SP",
            "extensions": {
                "kokoro": {
                    "speech_patterns": {"angry": "sharp words"},
                },
            },
        }
        result = map_v2_yaml_to_db(data)
        assert result["speech_patterns"] == ""

    def test_speech_patterns_string_used_directly(self):
        data = {
            "name": "SP",
            "extensions": {
                "kokoro": {
                    "speech_patterns": "always whispers",
                },
            },
        }
        result = map_v2_yaml_to_db(data)
        assert result["speech_patterns"] == "always whispers"

    def test_speech_patterns_none_gives_empty(self):
        data = {"name": "SP", "extensions": {"kokoro": {}}}
        result = map_v2_yaml_to_db(data)
        assert result["speech_patterns"] == ""

    def test_kokoro_extensions_nested_in_extensions_json(self):
        data = {
            "name": "Ext",
            "extensions": {
                "kokoro": {
                    "speech_patterns": {"default": "x", "happy": "y"},
                    "affection_overrides": {"lover": "sweet"},
                    "post_history_by_level": {"stranger": "cold"},
                    "anti_patterns": [{"bad": "a", "correct": "b", "why": "c"}],
                },
            },
        }
        result = map_v2_yaml_to_db(data)
        ext = json.loads(result["extensions_json"])

        assert ext["speech_patterns"] == {"default": "x", "happy": "y"}
        assert ext["affection_overrides"] == {"lover": "sweet"}
        assert ext["post_history_by_level"] == {"stranger": "cold"}
        assert ext["anti_patterns"] == [{"bad": "a", "correct": "b", "why": "c"}]

    def test_non_kokoro_extensions_merged(self):
        data = {
            "name": "NKE",
            "extensions": {
                "kokoro": {"mbti": "INTJ"},
                "custom_plugin": {"version": 2},
                "another": "hello",
            },
        }
        result = map_v2_yaml_to_db(data)
        ext = json.loads(result["extensions_json"])

        assert ext["custom_plugin"] == {"version": 2}
        assert ext["another"] == "hello"

    def test_kokoro_stories_excluded_from_extensions_json(self):
        data = {
            "name": "Stories",
            "extensions": {
                "kokoro": {"mbti": "INFP"},
                "kokoro_stories": [{"title": "A story"}],
                "other_ext": {"val": 1},
            },
        }
        result = map_v2_yaml_to_db(data)
        ext = json.loads(result["extensions_json"])

        assert "kokoro_stories" not in ext
        assert ext["other_ext"] == {"val": 1}

    def test_ensure_ascii_false_preserves_unicode(self):
        data = {
            "name": "Unicode",
            "extensions": {
                "kokoro": {
                    "likes": ["花", "茶道"],
                },
            },
        }
        result = map_v2_yaml_to_db(data)
        # ensure_ascii=False means raw unicode, not \\uXXXX escapes
        assert "花" in result["likes"]
        assert "\\u" not in result["likes"]

    def test_empty_extensions_no_extensions_json(self):
        data = {"name": "NoExt"}
        result = map_v2_yaml_to_db(data)
        assert result["extensions_json"] is None

    def test_extensions_none_handled(self):
        data = {"name": "NoneExt", "extensions": None}
        result = map_v2_yaml_to_db(data)
        assert result["extensions_json"] is None

    def test_personality_falls_back_to_description(self):
        data = {"name": "FB", "description": "my desc"}
        result = map_v2_yaml_to_db(data)
        assert result["personality"] == "my desc"

    def test_extra_kokoro_keys_in_extensions_json(self):
        """Kokoro keys not in the standard set should appear in extensions_json."""
        data = {
            "name": "Extra",
            "extensions": {
                "kokoro": {
                    "mbti": "INTP",          # standard, goes to column
                    "custom_field": "hello",  # non-standard
                    "another_custom": 42,     # non-standard
                },
            },
        }
        result = map_v2_yaml_to_db(data)
        ext = json.loads(result["extensions_json"])

        assert ext["custom_field"] == "hello"
        assert ext["another_custom"] == 42
