"""Unit tests for ConfigStore.

All tests use pytest's tmp_path fixture so they never touch the real
~/.skilark directory on the developer's machine.
"""

import os
import stat

import pytest

from skilark_cli.config_store import ConfigStore, DEFAULT_API_URL, validate_api_url


class TestIsFirstRun:
    def test_first_run_detected_when_no_file(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        assert store.is_first_run()

    def test_not_first_run_after_save(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["python"])
        assert not store.is_first_run()


class TestSaveAndLoad:
    def test_save_and_load_roundtrip(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(
            user_id="abc-123",
            topics=["python", "kubernetes"],
            api_url="https://api.skilark.com",
        )
        config = store.load()
        assert config["user_id"] == "abc-123"
        assert config["topics"] == ["python", "kubernetes"]
        assert config["api_url"] == "https://api.skilark.com"

    def test_save_uses_default_api_url_when_omitted(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["python"])
        config = store.load()
        assert config["api_url"] == DEFAULT_API_URL

    def test_save_creates_config_dir_if_missing(self, tmp_path):
        nested = tmp_path / "deep" / "nested"
        store = ConfigStore(config_dir=nested)
        store.save(user_id="x", topics=[])
        assert store.config_file.exists()

    def test_save_overwrites_existing_config(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="old-id", topics=["java"])
        store.save(user_id="new-id", topics=["go"])
        config = store.load()
        assert config["user_id"] == "new-id"
        assert config["topics"] == ["go"]

    def test_load_raises_when_no_file(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        with pytest.raises(FileNotFoundError):
            store.load()

    def test_save_empty_topics_list(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=[])
        config = store.load()
        assert config["topics"] == []


class TestUpdateTopics:
    def test_update_topics_replaces_list(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["python"])
        store.update_topics(["python", "kubernetes", "spark"])
        config = store.load()
        assert config["topics"] == ["python", "kubernetes", "spark"]

    def test_update_topics_preserves_other_fields(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(
            user_id="abc-123",
            topics=["python"],
            api_url="https://api.skilark.com",
        )
        store.update_topics(["go"])
        config = store.load()
        # Ensure user_id and api_url are untouched
        assert config["user_id"] == "abc-123"
        assert config["api_url"] == "https://api.skilark.com"

    def test_update_topics_raises_when_no_file(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        with pytest.raises(FileNotFoundError):
            store.update_topics(["python"])


class TestFilePermissions:
    def test_config_file_is_owner_only(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["python"])
        mode = os.stat(store.config_file).st_mode
        assert mode & stat.S_IRWXG == 0, "group should have no access"
        assert mode & stat.S_IRWXO == 0, "others should have no access"

    def test_config_file_permissions_preserved_on_update(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["python"])
        store.update_topics(["go"])
        mode = os.stat(store.config_file).st_mode
        assert mode & stat.S_IRWXG == 0, "group should have no access"
        assert mode & stat.S_IRWXO == 0, "others should have no access"


class TestValidateApiUrl:
    def test_accepts_production_url(self):
        assert validate_api_url("https://api.skilark.com") == "https://api.skilark.com"

    def test_accepts_localhost_http(self):
        assert validate_api_url("http://localhost:8000") == "http://localhost:8000"

    def test_accepts_127_0_0_1_http(self):
        assert validate_api_url("http://127.0.0.1:8000") == "http://127.0.0.1:8000"

    def test_rejects_http_for_remote_host(self):
        with pytest.raises(ValueError, match="must use HTTPS"):
            validate_api_url("http://api.skilark.com")

    def test_rejects_unknown_host(self):
        with pytest.raises(ValueError, match="host not allowed"):
            validate_api_url("https://evil.example.com")

    def test_rejects_ftp_scheme(self):
        with pytest.raises(ValueError, match="must use HTTPS"):
            validate_api_url("ftp://api.skilark.com")

    def test_rejects_ftp_for_localhost(self):
        with pytest.raises(ValueError, match="must use http or https"):
            validate_api_url("ftp://localhost:8000")

    def test_save_rejects_invalid_api_url(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        with pytest.raises(ValueError, match="host not allowed"):
            store.save(user_id="abc", topics=[], api_url="https://evil.com")

    def test_load_rejects_tampered_api_url(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc", topics=[], api_url="https://api.skilark.com")
        # Tamper with the config file directly
        store.config_file.write_text(
            "user_id: abc\ntopics: []\napi_url: https://evil.com\n"
        )
        with pytest.raises(ValueError, match="host not allowed"):
            store.load()


class TestUpdateCodingLanguage:
    def test_save_and_load_with_coding_language(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["dsa"], api_url="http://localhost:8000")
        store.update_coding_language("python")
        config = store.load()
        assert config["coding_language"] == "python"

    def test_update_coding_language_preserves_other_fields(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["dsa", "python"], api_url="http://localhost:8000")
        store.update_coding_language("java")
        config = store.load()
        assert config["user_id"] == "abc-123"
        assert config["topics"] == ["dsa", "python"]
        assert config["coding_language"] == "java"

    def test_coding_language_defaults_to_none(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["dsa"], api_url="http://localhost:8000")
        config = store.load()
        assert config.get("coding_language") is None


class TestUpdateTheme:
    def test_save_and_load_theme(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["python"])
        store.update_theme("light")
        config = store.load()
        assert config["theme"] == "light"

    def test_update_theme_preserves_other_fields(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=["python"])
        store.update_theme("dark")
        config = store.load()
        assert config["user_id"] == "abc-123"
        assert config["topics"] == ["python"]

    def test_theme_defaults_to_none(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=[])
        config = store.load()
        assert config.get("theme") is None

    def test_update_theme_raises_when_no_file(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        with pytest.raises(FileNotFoundError):
            store.update_theme("light")

    def test_update_theme_accepts_any_textual_theme(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="abc-123", topics=[])
        store.update_theme("dracula")
        assert store.load()["theme"] == "dracula"


class TestDrillState:
    def test_save_and_load_drill_state(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=["python"])
        store.save_drill_state({
            "view": "code",
            "topic": None,
            "category": "arrays",
            "problem_id": "p1",
        })
        state = store.get_drill_state()
        assert state["view"] == "code"
        assert state["category"] == "arrays"
        assert state["problem_id"] == "p1"
        assert state["topic"] is None

    def test_get_drill_state_returns_none_when_missing(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        assert store.get_drill_state() is None

    def test_get_drill_state_rejects_invalid_view(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        store.save_drill_state({"view": "bogus", "topic": "python"})
        assert store.get_drill_state() is None

    def test_get_drill_state_rejects_missing_view(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        store.save_drill_state({"topic": "python"})
        assert store.get_drill_state() is None

    def test_save_drill_state_noop_on_first_run(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save_drill_state({"view": "quiz", "topic": "python"})
        assert store.is_first_run()  # No crash, no file created

    def test_get_drill_state_empty_dict_returns_none(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        store.save_drill_state({})
        assert store.get_drill_state() is None

    def test_clear_drill_state(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        store.save_drill_state({"view": "quiz", "topic": "python"})
        store.clear_drill_state()
        assert store.get_drill_state() is None


class TestLastTab:
    def test_save_and_load_last_tab(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        store.save_last_tab(2)
        assert store.get_last_tab() == 2

    def test_get_last_tab_returns_zero_when_missing(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        assert store.get_last_tab() == 0

    def test_save_last_tab_noop_on_first_run(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save_last_tab(2)
        assert store.is_first_run()  # No crash, no file created

    def test_last_tab_clamps_to_valid_range(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        store.save_last_tab(99)
        assert store.get_last_tab() == 0  # invalid, fallback to 0


class TestSelfDescription:
    def test_save_and_load_self_description(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=["python"])
        store.update_profile(self_description="Senior backend engineer with 10 years experience")
        assert store.get_self_description() == "Senior backend engineer with 10 years experience"

    def test_get_self_description_returns_empty_when_not_set(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        assert store.get_self_description() == ""

    def test_get_self_description_returns_empty_on_first_run(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        assert store.get_self_description() == ""

    def test_update_profile_self_description_preserves_other_fields(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=["python"])
        store.update_profile(current_role="swe")
        store.update_profile(self_description="My background")
        config = store.load()
        assert config["current_role"] == "swe"
        assert config["self_description"] == "My background"
        assert config["user_id"] == "u1"

    def test_self_description_can_be_cleared(self, tmp_path):
        store = ConfigStore(config_dir=tmp_path)
        store.save(user_id="u1", topics=[])
        store.update_profile(self_description="Something")
        store.update_profile(self_description="")
        assert store.get_self_description() == ""
