"""Settings API endpoint tests.

Tests the /settings, /settings/schema, and POST /settings endpoints
using an in-process FastAPI TestClient.
"""

from __future__ import annotations

from unittest.mock import patch

import pytest
from starlette.testclient import TestClient

from dictare.config import Config


@pytest.fixture
def settings_app():
    """Create a minimal FastAPI app with settings routes for testing."""
    from unittest.mock import MagicMock

    from dictare.core.http_server import OpenVIPServer

    engine = MagicMock()
    engine.get_status.return_value = {"state": "OFF"}
    server = OpenVIPServer(engine, controller=None, host="127.0.0.1", port=0)
    app = server._create_app()
    return app

@pytest.fixture
def client(settings_app):
    """TestClient for the FastAPI app."""
    return TestClient(settings_app)

class TestGetSettingsPage:
    """GET /settings — redirects to SPA or serves fallback HTML."""

    def test_settings_redirects_to_ui(self, client):
        r = client.get("/settings", follow_redirects=False)
        assert r.status_code in (301, 302, 307, 308)
        assert "/ui" in r.headers.get("location", "")

    def test_ui_serves_html(self, client):
        r = client.get("/ui/")
        assert r.status_code == 200
        assert "text/html" in r.headers["content-type"]

    def test_ui_contains_sveltekit_app(self, client):
        r = client.get("/ui/")
        html = r.text
        assert "<title>Dictare Settings</title>" in html

class TestGetSettingsSchema:
    """GET /settings/schema — returns JSON Schema + current values."""

    def test_returns_schema(self, client):
        r = client.get("/api/settings/schema")
        assert r.status_code == 200
        data = r.json()
        assert "schema" in data
        assert "values" in data
        assert "keys" in data
        assert "version" in data

    def test_schema_has_definitions(self, client):
        r = client.get("/api/settings/schema")
        schema = r.json()["schema"]
        assert "$defs" in schema
        assert "TTSConfig" in schema["$defs"]
        assert "AudioConfig" in schema["$defs"]

    def test_non_string_values_keep_pydantic_defaults(self, client, tmp_path):
        """Bool/number fields keep their Pydantic-resolved defaults."""
        config_file = tmp_path / "config.toml"
        config_file.write_text("")  # empty TOML — pure Pydantic defaults
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/schema")
            values = r.json()["values"]
            config = Config()
            assert values["audio"]["advanced"]["sample_rate"] == config.audio.advanced.sample_rate
            assert values["audio"]["audio_feedback"] == config.audio.audio_feedback

    def test_string_values_empty_when_not_in_toml(self, client, tmp_path):
        """String fields not set in TOML are returned as "" (use default)."""
        config_file = tmp_path / "config.toml"
        config_file.write_text("")  # empty TOML — nothing set
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/schema")
        values = r.json()["values"]
        # stt.language has Pydantic default "auto" but not in TOML → ""
        assert values["stt"]["language"] == ""
        # stt.model has Pydantic default "large-v3-turbo" but not in TOML → ""
        assert values["stt"]["model"] == ""
        # tts.language has a Pydantic default but not in TOML → ""
        assert values["tts"]["language"] == ""

    def test_string_values_present_when_in_toml(self, client, tmp_path):
        """String fields explicitly set in TOML are returned with their value."""
        config_file = tmp_path / "config.toml"
        config_file.write_text('[stt]\nlanguage = "it"\nmodel = "base"\n')
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/schema")
        values = r.json()["values"]
        assert values["stt"]["language"] == "it"
        assert values["stt"]["model"] == "base"

    def test_mixed_set_and_unset_fields(self, client, tmp_path):
        """Only explicitly set string fields have values; others are ""."""
        config_file = tmp_path / "config.toml"
        config_file.write_text('[stt]\nlanguage = "en"\n')
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/schema")
        values = r.json()["values"]
        assert values["stt"]["language"] == "en"  # set → value
        assert values["stt"]["model"] == ""  # not set → ""
        assert values["tts"]["language"] == ""  # not set → ""

    def test_value_matching_default_still_returned(self, client, tmp_path):
        """If user explicitly sets value = Pydantic default, it's returned (not "")."""
        config_file = tmp_path / "config.toml"
        # "auto" is the Pydantic default for stt.language, but user set it explicitly
        config_file.write_text('[stt]\nlanguage = "auto"\n')
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/schema")
        values = r.json()["values"]
        assert values["stt"]["language"] == "auto"  # explicitly set, even if = default

    def test_keys_list_has_entries(self, client):
        r = client.get("/api/settings/schema")
        keys = r.json()["keys"]
        assert len(keys) > 10
        # Each key has required fields
        k = keys[0]
        assert "key" in k
        assert "type" in k
        assert "description" in k

    def test_tts_engine_has_enum(self, client):
        r = client.get("/api/settings/schema")
        schema = r.json()["schema"]
        tts = schema["$defs"]["TTSConfig"]
        engine_field = tts["properties"]["engine"]
        assert "enum" in engine_field
        assert "say" in engine_field["enum"]
        assert "espeak" in engine_field["enum"]

class TestDefaultRoundtrip:
    """Verify save→reload cycle preserves "Default" vs explicit values."""

    def test_save_value_then_schema_returns_it(self, client, tmp_path):
        """Save a non-default value → schema returns that value."""
        config_file = tmp_path / "config.toml"
        config_file.write_text("")
        with patch("dictare.config.get_config_path", return_value=config_file):
            # Save stt.language = "it"
            r = client.post("/api/settings", json={"key": "stt.language", "value": "it"})
            assert r.status_code == 200
            # Reload schema
            r = client.get("/api/settings/schema")
            values = r.json()["values"]
            assert values["stt"]["language"] == "it"

    def test_save_empty_resets_to_default(self, client, tmp_path):
        """Save "" (reset to default) → schema returns ""."""
        config_file = tmp_path / "config.toml"
        config_file.write_text('[stt]\nlanguage = "it"\n')
        with patch("dictare.config.get_config_path", return_value=config_file):
            # Reset to default
            r = client.post("/api/settings", json={"key": "stt.language", "value": ""})
            assert r.status_code == 200
            # Reload schema — should be "" not "auto"
            r = client.get("/api/settings/schema")
            values = r.json()["values"]
            assert values["stt"]["language"] == ""

    def test_save_default_value_explicitly(self, client, tmp_path):
        """Save a value that equals the Pydantic default → schema returns that value (not "")."""
        config_file = tmp_path / "config.toml"
        config_file.write_text("")
        with patch("dictare.config.get_config_path", return_value=config_file):
            # Explicitly save "auto" (which is the Pydantic default for stt.language)
            r = client.post("/api/settings", json={"key": "stt.language", "value": "auto"})
            assert r.status_code == 200
            # Schema should return "auto" — user chose it explicitly
            r = client.get("/api/settings/schema")
            values = r.json()["values"]
            assert values["stt"]["language"] == "auto"

    def test_bool_field_unaffected(self, client, tmp_path):
        """Bool fields always return their resolved value, not ""."""
        config_file = tmp_path / "config.toml"
        config_file.write_text("")  # nothing set
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/schema")
            values = r.json()["values"]
            # audio.audio_feedback default is True — should stay True, not ""
            assert values["audio"]["audio_feedback"] is True

    def test_number_field_unaffected(self, client, tmp_path):
        """Number fields always return their resolved value, not ""."""
        config_file = tmp_path / "config.toml"
        config_file.write_text("")
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/schema")
            values = r.json()["values"]
            assert values["output"]["typing_delay_ms"] == 2

class TestPostSettings:
    """POST /settings — update a config value."""

    def test_update_valid_key(self, client, tmp_path):
        config_file = tmp_path / "config.toml"
        config_file.write_text("[stt]\nmodel = \"large-v3-turbo\"\n")
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.post(
                "/api/settings",
                json={"key": "stt.model", "value": "base"},
            )
            assert r.status_code == 200
            data = r.json()
            assert data["status"] == "ok"
            assert data["key"] == "stt.model"

    def test_invalid_key_returns_404(self, client):
        r = client.post(
            "/api/settings",
            json={"key": "nonexistent.field", "value": "foo"},
        )
        assert r.status_code == 404

    def test_missing_key_returns_400(self, client):
        r = client.post("/api/settings", json={"value": "foo"})
        assert r.status_code == 400

    def test_invalid_value_returns_400(self, client, tmp_path):
        config_file = tmp_path / "config.toml"
        config_file.write_text("[output]\nmode = \"keyboard\"\n")
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.post(
                "/api/settings",
                json={"key": "output.mode", "value": "invalid_mode"},
            )
            assert r.status_code == 400

class TestTomlSectionGet:
    """GET /settings/toml-section/{section}"""

    def test_agent_profiles_returns_content_from_file(self, client, tmp_path):
        """Fetch returns the raw content from the config file (WYSIWYG)."""
        config_file = tmp_path / "config.toml"
        config_file.write_text(
            '[agent_profiles.claude]\ncommand = ["claude"]\n'
        )
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/toml-section/agent_profiles")
        assert r.status_code == 200
        data = r.json()
        assert data["section"] == "agent_profiles"
        assert "agent_profiles" in data["content"]
        assert "claude" in data["content"]

    def test_agent_profiles_returns_template_when_absent(self, client, tmp_path):
        """Fetch returns the default preset template when the section is not in the file."""
        config_file = tmp_path / "config.toml"
        config_file.write_text("")  # no agent_profiles section
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/toml-section/agent_profiles")
        assert r.status_code == 200
        content = r.json()["content"]
        # Template includes pre-filled presets
        assert "[agent_profiles]" in content
        assert "sonnet" in content
        assert "opus" in content
        assert "chatgpt" in content

    def test_shortcuts_returns_content(self, client):
        r = client.get("/api/settings/toml-section/keyboard.shortcuts")
        assert r.status_code == 200
        data = r.json()
        assert data["section"] == "keyboard.shortcuts"
        assert isinstance(data["content"], str)

    def test_unknown_section_returns_404(self, client):
        r = client.get("/api/settings/toml-section/nonexistent")
        assert r.status_code == 404

    def test_fallback_template_has_defaults(self, client, tmp_path):
        """Template header (when no section in file) contains default presets."""
        config_file = tmp_path / "nonexistent_config.toml"
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/toml-section/agent_profiles")
        assert r.status_code == 200
        assert "[agent_profiles]" in r.json()["content"]

class TestTomlSectionPost:
    """POST /settings/toml-section/{section}"""

    def test_save_agent_profiles(self, client, tmp_path):
        config_file = tmp_path / "config.toml"
        config_file.write_text("")
        toml_content = """
[agent_profiles]
default = "claude"

[agent_profiles.claude]
command = ["claude"]
description = "Claude Code"
"""
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.post(
                "/api/settings/toml-section/agent_profiles",
                json={"content": toml_content},
            )
        assert r.status_code == 200
        assert r.json()["status"] == "ok"

        # Verify persisted
        from dictare.config import load_config
        config = load_config(config_file)
        assert config.agent_profiles.default == "claude"
        assert "claude" in config.agent_profiles
        assert config.agent_profiles.get("claude").command == ["claude"]

    def test_save_agent_profiles_preserves_continue_args(self, client, tmp_path):
        """continue_args must survive a round-trip through the TOML editor."""
        config_file = tmp_path / "config.toml"
        config_file.write_text("")
        toml_content = """
[agent_profiles.sonnet]
command = ["claude", "--model", "claude-sonnet-4-6"]
continue_args = ["-c"]
description = "Claude Sonnet"
"""
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.post(
                "/api/settings/toml-section/agent_profiles",
                json={"content": toml_content},
            )
        assert r.status_code == 200

        from dictare.config import load_config
        config = load_config(config_file)
        assert config.agent_profiles.get("sonnet").continue_args == ["-c"]

    def test_serialize_agent_profiles_includes_continue_args(self, client, tmp_path):
        """GET agent_profiles returns continue_args from the raw config file."""
        config_file = tmp_path / "config.toml"
        config_file.write_text(
            '[agent_profiles.sonnet]\n'
            'command = ["claude", "--model", "claude-sonnet-4-6"]\n'
            'continue_args = ["-c"]\n'
        )
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/toml-section/agent_profiles")
        assert r.status_code == 200
        assert "continue_args" in r.json()["content"]
        assert '"-c"' in r.json()["content"]

    def test_fetch_preserves_user_comments(self, client, tmp_path):
        """GET agent_profiles preserves comments the user added to the file."""
        config_file = tmp_path / "config.toml"
        config_file.write_text(
            "# my custom comment\n"
            "[agent_profiles.sonnet]\n"
            'command = ["claude"]\n'
        )
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.get("/api/settings/toml-section/agent_profiles")
        assert r.status_code == 200
        assert "my custom comment" in r.json()["content"]

    def test_invalid_toml_returns_400(self, client):
        r = client.post(
            "/api/settings/toml-section/agent_profiles",
            json={"content": "[[[ invalid toml ==="},
        )
        assert r.status_code == 400

    def test_missing_command_returns_400(self, client, tmp_path):
        config_file = tmp_path / "config.toml"
        config_file.write_text("")
        toml_content = """
[agent_profiles.claude]
description = "Missing command field"
"""
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.post(
                "/api/settings/toml-section/agent_profiles",
                json={"content": toml_content},
            )
        assert r.status_code == 400

    def test_empty_content_returns_400(self, client):
        r = client.post(
            "/api/settings/toml-section/agent_profiles",
            json={"content": "   "},
        )
        assert r.status_code == 400

    def test_unknown_section_returns_404(self, client):
        r = client.post(
            "/api/settings/toml-section/nonexistent",
            json={"content": "foo = 1"},
        )
        assert r.status_code == 404

    def test_save_shortcuts(self, client, tmp_path):
        config_file = tmp_path / "config.toml"
        config_file.write_text("")
        toml_content = """
[[keyboard.shortcuts]]
keys = "ctrl+shift+l"
command = "toggle-listening"
"""
        with patch("dictare.config.get_config_path", return_value=config_file):
            r = client.post(
                "/api/settings/toml-section/keyboard.shortcuts",
                json={"content": toml_content},
            )
        assert r.status_code == 200

        from dictare.config import load_config
        config = load_config(config_file)
        assert len(config.keyboard.shortcuts) == 1
        assert config.keyboard.shortcuts[0]["keys"] == "ctrl+shift+l"
        assert config.keyboard.shortcuts[0]["command"] == "toggle-listening"
