"""Tests for safeconfig.core.loaders."""

from __future__ import annotations

import json
from pathlib import Path

import pytest
import yaml

from pysafeconfigx.core.exceptions import ConfigurationError
from pysafeconfigx.core.loaders import DotEnvLoader, EnvironmentLoader, JsonLoader, YamlLoader


class TestDotEnvLoader:
    def test_loads_basic_env(self, tmp_path: Path) -> None:
        p = tmp_path / ".env"
        p.write_text("FOO=bar\nBAZ=qux\n", encoding="utf-8")
        loader = DotEnvLoader(p)
        data = loader.load()
        assert data["FOO"] == "bar"
        assert data["BAZ"] == "qux"

    def test_missing_file_returns_empty(self, tmp_path: Path) -> None:
        loader = DotEnvLoader(tmp_path / ".env.missing")
        data = loader.load()
        assert data == {}

    def test_caches_on_second_call(self, tmp_path: Path) -> None:
        p = tmp_path / ".env"
        p.write_text("KEY=value\n", encoding="utf-8")
        loader = DotEnvLoader(p)
        data1 = loader.load()
        # Mutate file — without force, we should get cached data.
        p.write_text("KEY=changed\n", encoding="utf-8")
        data2 = loader.load()
        assert data1 is data2  # Same object (cached)

    def test_force_reload(self, tmp_path: Path) -> None:
        p = tmp_path / ".env"
        p.write_text("KEY=original\n", encoding="utf-8")
        loader = DotEnvLoader(p)
        loader.load()
        p.write_text("KEY=updated\n", encoding="utf-8")
        data = loader.load(force=True)
        assert data["KEY"] == "updated"

    def test_quoted_values(self, tmp_path: Path) -> None:
        p = tmp_path / ".env"
        p.write_text('MSG="hello world"\n', encoding="utf-8")
        data = DotEnvLoader(p).load()
        assert data["MSG"] == "hello world"

    def test_comments_ignored(self, tmp_path: Path) -> None:
        p = tmp_path / ".env"
        p.write_text("# This is a comment\nKEY=value\n", encoding="utf-8")
        data = DotEnvLoader(p).load()
        assert "# This is a comment" not in data


class TestJsonLoader:
    def test_loads_flat_json(self, tmp_path: Path) -> None:
        p = tmp_path / "config.json"
        p.write_text(json.dumps({"DATABASE_URL": "postgres://db", "PORT": 5432}), encoding="utf-8")
        data = JsonLoader(p).load()
        assert data["DATABASE_URL"] == "postgres://db"
        assert data["PORT"] == "5432"

    def test_flattens_nested_json(self, tmp_path: Path) -> None:
        p = tmp_path / "config.json"
        p.write_text(json.dumps({"database": {"url": "postgres://db", "pool": {"size": 5}}}), encoding="utf-8")
        data = JsonLoader(p).load()
        assert data["database.url"] == "postgres://db"
        assert data["database.pool.size"] == "5"

    def test_invalid_json_raises(self, tmp_path: Path) -> None:
        p = tmp_path / "bad.json"
        p.write_text("not json {", encoding="utf-8")
        with pytest.raises(ConfigurationError, match="valid JSON"):
            JsonLoader(p).load()

    def test_non_object_json_raises(self, tmp_path: Path) -> None:
        p = tmp_path / "bad.json"
        p.write_text("[1, 2, 3]", encoding="utf-8")
        with pytest.raises(ConfigurationError, match="JSON object"):
            JsonLoader(p).load()

    def test_missing_file_returns_empty(self, tmp_path: Path) -> None:
        data = JsonLoader(tmp_path / "nonexistent.json").load()
        assert data == {}


class TestYamlLoader:
    def test_loads_flat_yaml(self, tmp_path: Path) -> None:
        p = tmp_path / "config.yaml"
        p.write_text("DATABASE_URL: postgres://db\nPORT: 5432\n", encoding="utf-8")
        data = YamlLoader(p).load()
        assert data["DATABASE_URL"] == "postgres://db"
        assert data["PORT"] == "5432"

    def test_flattens_nested_yaml(self, tmp_path: Path) -> None:
        p = tmp_path / "config.yaml"
        content = yaml.dump({"database": {"url": "postgres://db", "pool": {"size": 5}}})
        p.write_text(content, encoding="utf-8")
        data = YamlLoader(p).load()
        assert data["database.url"] == "postgres://db"
        assert data["database.pool.size"] == "5"

    def test_empty_yaml_returns_empty(self, tmp_path: Path) -> None:
        p = tmp_path / "empty.yaml"
        p.write_text("", encoding="utf-8")
        data = YamlLoader(p).load()
        assert data == {}

    def test_invalid_yaml_raises(self, tmp_path: Path) -> None:
        p = tmp_path / "bad.yaml"
        p.write_text("key: [unclosed bracket\n", encoding="utf-8")
        with pytest.raises(ConfigurationError, match="valid YAML"):
            YamlLoader(p).load()

    def test_missing_file_returns_empty(self, tmp_path: Path) -> None:
        data = YamlLoader(tmp_path / "nonexistent.yaml").load()
        assert data == {}


class TestEnvironmentLoader:
    def test_loads_all_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("TEST_SAFECONFIG_VAR", "hello")
        loader = EnvironmentLoader()
        data = loader.load()
        assert data["TEST_SAFECONFIG_VAR"] == "hello"

    def test_prefix_filtering(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("MYAPP_KEY", "myvalue")
        monkeypatch.setenv("OTHER_KEY", "other")
        loader = EnvironmentLoader(prefix="MYAPP_")
        data = loader.load()
        assert "KEY" in data
        assert data["KEY"] == "myvalue"
        assert "OTHER_KEY" not in data

    def test_always_exists(self) -> None:
        assert EnvironmentLoader().exists() is True
