"""Tests for schema default stripping and application.

Tests for _strip_schema_defaults (removing defaults before extraction) and
_apply_schema_defaults (applying defaults to wizard state after extraction).
"""

from typing import Any

import pytest

from dataknobs_bots.reasoning.wizard import WizardReasoning, WizardState
from dataknobs_bots.reasoning.wizard_loader import WizardConfigLoader
from dataknobs_llm.testing import ConfigurableExtractor, SimpleExtractionResult


@pytest.fixture
def minimal_wizard_config() -> dict[str, Any]:
    """Create minimal wizard config for testing."""
    return {
        "name": "test-wizard",
        "stages": [
            {
                "name": "start",
                "is_start": True,
                "is_end": True,
                "prompt": "Test",
            }
        ],
    }


@pytest.fixture
def wizard_reasoning(minimal_wizard_config: dict[str, Any]) -> WizardReasoning:
    """Create WizardReasoning instance for testing."""
    loader = WizardConfigLoader()
    wizard_fsm = loader.load_from_dict(minimal_wizard_config)
    return WizardReasoning(wizard_fsm=wizard_fsm, strict_validation=False)


class TestStripSchemaDefaults:
    """Tests for _strip_schema_defaults method."""

    def test_strip_simple_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test stripping defaults from simple properties."""
        schema: dict[str, Any] = {
            "type": "object",
            "properties": {
                "provider": {"type": "string", "default": "anthropic"},
                "model": {"type": "string"},
            },
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        assert "default" not in result["properties"]["provider"]
        assert result["properties"]["model"]["type"] == "string"

    def test_strip_nested_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test stripping defaults from nested object properties."""
        schema: dict[str, Any] = {
            "type": "object",
            "properties": {
                "config": {
                    "type": "object",
                    "properties": {
                        "temperature": {"type": "number", "default": 0.7}
                    },
                }
            },
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        nested_temp = result["properties"]["config"]["properties"]["temperature"]
        assert "default" not in nested_temp

    def test_strip_array_item_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test stripping defaults from array item schemas."""
        schema: dict[str, Any] = {
            "type": "object",
            "properties": {
                "items": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "enabled": {"type": "boolean", "default": True}
                        },
                    },
                }
            },
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        item_enabled = result["properties"]["items"]["items"]["properties"]["enabled"]
        assert "default" not in item_enabled

    def test_original_schema_unchanged(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test that original schema is not modified."""
        schema: dict[str, Any] = {
            "type": "object",
            "properties": {"value": {"type": "string", "default": "test"}},
        }

        wizard_reasoning._strip_schema_defaults(schema)

        # Original should be unchanged
        assert schema["properties"]["value"]["default"] == "test"

    def test_strip_allof_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test stripping defaults from allOf schemas."""
        schema: dict[str, Any] = {
            "allOf": [
                {"properties": {"field": {"type": "string", "default": "x"}}}
            ]
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        assert "default" not in result["allOf"][0]["properties"]["field"]

    def test_strip_anyof_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test stripping defaults from anyOf schemas."""
        schema: dict[str, Any] = {
            "anyOf": [
                {"properties": {"a": {"type": "string", "default": "x"}}},
                {"properties": {"b": {"type": "number", "default": 5}}},
            ]
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        assert "default" not in result["anyOf"][0]["properties"]["a"]
        assert "default" not in result["anyOf"][1]["properties"]["b"]

    def test_strip_oneof_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test stripping defaults from oneOf schemas."""
        schema: dict[str, Any] = {
            "oneOf": [
                {"properties": {"x": {"type": "string", "default": "hello"}}},
            ]
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        assert "default" not in result["oneOf"][0]["properties"]["x"]

    def test_preserves_other_schema_properties(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test that non-default properties are preserved."""
        schema: dict[str, Any] = {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "default": "test",
                    "description": "User's name",
                    "minLength": 1,
                    "maxLength": 100,
                }
            },
            "required": ["name"],
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        prop = result["properties"]["name"]
        assert "default" not in prop
        assert prop["description"] == "User's name"
        assert prop["minLength"] == 1
        assert prop["maxLength"] == 100
        assert result["required"] == ["name"]

    def test_schema_without_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test schema with no defaults returns equivalent copy."""
        schema: dict[str, Any] = {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
            },
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        assert result["properties"]["name"]["type"] == "string"
        assert result["properties"]["age"]["type"] == "integer"
        # Should be a copy, not the same object
        assert result is not schema

    def test_empty_properties(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test schema with empty properties dict."""
        schema: dict[str, Any] = {"type": "object", "properties": {}}

        result = wizard_reasoning._strip_schema_defaults(schema)

        assert result["properties"] == {}

    def test_deeply_nested_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test stripping defaults from deeply nested structures."""
        schema: dict[str, Any] = {
            "type": "object",
            "properties": {
                "level1": {
                    "type": "object",
                    "properties": {
                        "level2": {
                            "type": "object",
                            "properties": {
                                "level3": {
                                    "type": "string",
                                    "default": "deep_value",
                                }
                            },
                        }
                    },
                }
            },
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        level3 = result["properties"]["level1"]["properties"]["level2"][
            "properties"
        ]["level3"]
        assert "default" not in level3
        assert level3["type"] == "string"

    def test_multiple_defaults_same_level(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Test stripping multiple defaults at the same nesting level."""
        schema: dict[str, Any] = {
            "type": "object",
            "properties": {
                "a": {"type": "string", "default": "val_a"},
                "b": {"type": "number", "default": 42},
                "c": {"type": "boolean", "default": True},
            },
        }

        result = wizard_reasoning._strip_schema_defaults(schema)

        assert "default" not in result["properties"]["a"]
        assert "default" not in result["properties"]["b"]
        assert "default" not in result["properties"]["c"]


def _recording_extractor() -> ConfigurableExtractor:
    """Create an extractor that records inputs and returns empty low-confidence results."""
    return ConfigurableExtractor(confidence=0.5)


class TestSchemaDefaultsIntegration:
    """Integration tests for schema defaults stripping."""

    @pytest.mark.asyncio
    async def test_extractor_receives_schema_without_defaults(self) -> None:
        """Extractor should receive schema with defaults stripped."""
        extractor = _recording_extractor()

        wizard_config: dict[str, Any] = {
            "name": "test-wizard",
            "stages": [
                {
                    "name": "configure_llm",
                    "is_start": True,
                    "is_end": True,
                    "prompt": "Choose your LLM",
                    "schema": {
                        "type": "object",
                        "properties": {
                            "provider": {"type": "string", "default": "anthropic"},
                            "model": {"type": "string"},
                        },
                    },
                }
            ],
        }

        loader = WizardConfigLoader()
        wizard_fsm = loader.load_from_dict(wizard_config)
        reasoning = WizardReasoning(
            wizard_fsm=wizard_fsm, extractor=extractor, strict_validation=False
        )

        stage = wizard_fsm.current_metadata
        await reasoning._extract_data("some message", stage, llm=None)

        # Verify extractor received schema without default
        assert len(extractor.extract_calls) > 0
        assert "default" not in extractor.extract_calls[0]["schema"]["properties"]["provider"]
        assert extractor.extract_calls[0]["schema"]["properties"]["model"]["type"] == "string"

    @pytest.mark.asyncio
    async def test_extraction_result_has_no_autofilled_defaults(self) -> None:
        """Verify that extraction doesn't produce values from schema defaults."""
        extractor = _recording_extractor()

        wizard_config: dict[str, Any] = {
            "name": "test-wizard",
            "stages": [
                {
                    "name": "llm_config",
                    "is_start": True,
                    "is_end": True,
                    "prompt": "Configure LLM",
                    "schema": {
                        "type": "object",
                        "properties": {
                            "provider": {"type": "string", "default": "anthropic"},
                            "model": {"type": "string", "default": "claude-3-sonnet"},
                        },
                    },
                }
            ],
        }

        loader = WizardConfigLoader()
        wizard_fsm = loader.load_from_dict(wizard_config)
        reasoning = WizardReasoning(
            wizard_fsm=wizard_fsm, extractor=extractor, strict_validation=False
        )

        stage = wizard_fsm.current_metadata
        result = await reasoning._extract_data(
            "I want to build a math tutor",  # No LLM info mentioned
            stage,
            llm=None,
        )

        # ConfigurableExtractor returns empty data
        # In real usage, even a real extractor shouldn't fill defaults
        assert "provider" not in result.data
        assert "model" not in result.data

    @pytest.mark.asyncio
    async def test_no_extractor_still_works(self) -> None:
        """When no extractor is configured, extraction still works."""
        wizard_config: dict[str, Any] = {
            "name": "test-wizard",
            "stages": [
                {
                    "name": "configure",
                    "is_start": True,
                    "is_end": True,
                    "prompt": "Configure",
                    "schema": {
                        "type": "object",
                        "properties": {
                            "value": {"type": "string", "default": "test"},
                        },
                    },
                }
            ],
        }

        loader = WizardConfigLoader()
        wizard_fsm = loader.load_from_dict(wizard_config)
        # No extractor provided
        reasoning = WizardReasoning(wizard_fsm=wizard_fsm, strict_validation=False)

        stage = wizard_fsm.current_metadata
        result = await reasoning._extract_data("user message", stage, llm=None)

        # Should return fallback result with raw input
        assert result.data == {"_raw_input": "user message"}
        assert result.confidence == 0.5

    @pytest.mark.asyncio
    async def test_no_schema_stage(self) -> None:
        """Stage without schema should pass through raw input."""
        wizard_config: dict[str, Any] = {
            "name": "test-wizard",
            "stages": [
                {
                    "name": "welcome",
                    "is_start": True,
                    "is_end": True,
                    "prompt": "Welcome!",
                    # No schema
                }
            ],
        }

        loader = WizardConfigLoader()
        wizard_fsm = loader.load_from_dict(wizard_config)
        reasoning = WizardReasoning(wizard_fsm=wizard_fsm, strict_validation=False)

        stage = wizard_fsm.current_metadata
        result = await reasoning._extract_data("hello", stage, llm=None)

        assert result.data == {"_raw_input": "hello"}
        assert result.confidence == 1.0


class TestApplySchemaDefaults:
    """Tests for _apply_schema_defaults method."""

    def test_applies_missing_defaults(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Defaults are applied for properties not in wizard data."""
        state = WizardState(
            current_stage="start",
            data={"topic": "biology"},
        )
        stage: dict[str, Any] = {
            "schema": {
                "type": "object",
                "properties": {
                    "topic": {"type": "string"},
                    "difficulty": {"type": "string", "default": "medium"},
                    "question_type": {
                        "type": "string",
                        "default": "multiple_choice",
                    },
                },
            }
        }

        applied = wizard_reasoning._apply_schema_defaults(state, stage)

        assert applied == {"difficulty", "question_type"}
        assert state.data["difficulty"] == "medium"
        assert state.data["question_type"] == "multiple_choice"
        # Existing value untouched
        assert state.data["topic"] == "biology"

    def test_does_not_overwrite_existing_values(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Existing non-None values are preserved."""
        state = WizardState(
            current_stage="start",
            data={"difficulty": "hard"},
        )
        stage: dict[str, Any] = {
            "schema": {
                "type": "object",
                "properties": {
                    "difficulty": {"type": "string", "default": "medium"},
                },
            }
        }

        applied = wizard_reasoning._apply_schema_defaults(state, stage)

        assert applied == set()
        assert state.data["difficulty"] == "hard"

    def test_applies_default_when_value_is_none(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Default is applied when value is explicitly None."""
        state = WizardState(
            current_stage="start",
            data={"difficulty": None},
        )
        stage: dict[str, Any] = {
            "schema": {
                "type": "object",
                "properties": {
                    "difficulty": {"type": "string", "default": "medium"},
                },
            }
        }

        applied = wizard_reasoning._apply_schema_defaults(state, stage)

        assert applied == {"difficulty"}
        assert state.data["difficulty"] == "medium"

    def test_no_schema_returns_empty(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Stage without schema returns empty set."""
        state = WizardState(current_stage="start", data={})
        stage: dict[str, Any] = {}

        applied = wizard_reasoning._apply_schema_defaults(state, stage)

        assert applied == set()

    def test_no_defaults_in_schema(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Schema without any defaults returns empty set."""
        state = WizardState(current_stage="start", data={})
        stage: dict[str, Any] = {
            "schema": {
                "type": "object",
                "properties": {
                    "topic": {"type": "string"},
                },
            }
        }

        applied = wizard_reasoning._apply_schema_defaults(state, stage)

        assert applied == set()

    def test_boolean_default_false(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Boolean false default is applied (not treated as missing)."""
        state = WizardState(current_stage="start", data={})
        stage: dict[str, Any] = {
            "schema": {
                "type": "object",
                "properties": {
                    "enabled": {"type": "boolean", "default": False},
                },
            }
        }

        applied = wizard_reasoning._apply_schema_defaults(state, stage)

        assert applied == {"enabled"}
        assert state.data["enabled"] is False

    def test_integer_default_zero(
        self, wizard_reasoning: WizardReasoning
    ) -> None:
        """Integer zero default is applied (not treated as missing)."""
        state = WizardState(current_stage="start", data={})
        stage: dict[str, Any] = {
            "schema": {
                "type": "object",
                "properties": {
                    "count": {"type": "integer", "default": 0},
                },
            }
        }

        applied = wizard_reasoning._apply_schema_defaults(state, stage)

        # 0 is falsy but not None — should still be applied since
        # the key is not in data at all
        assert applied == {"count"}
        assert state.data["count"] == 0
