"""Tests for TOML config loading, precedence, and masking."""

from __future__ import annotations

import logging
from pathlib import Path
from unittest.mock import patch

import pytest

from certisigma_census.config import (
    _deep_merge,
    _find_project_config,
    config_paths,
    init_config,
    load_config,
    mask_secret_tail,
    mask_sensitive,
)


class TestDeepMerge:
    def test_flat_merge(self):
        result = _deep_merge({"a": 1, "b": 2}, {"b": 3, "c": 4})
        assert result == {"a": 1, "b": 3, "c": 4}

    def test_nested_merge(self):
        base = {"scan": {"include": [], "max_size": "1G"}}
        overlay = {"scan": {"include": ["*.pdf"]}}
        result = _deep_merge(base, overlay)
        assert result["scan"]["include"] == ["*.pdf"]
        assert result["scan"]["max_size"] == "1G"

    def test_does_not_mutate_base(self):
        base = {"a": {"b": 1}}
        overlay = {"a": {"b": 2}}
        _deep_merge(base, overlay)
        assert base["a"]["b"] == 1


class TestMaskSecretTail:
    def test_short_value_fully_masked(self):
        assert mask_secret_tail("abc") == "****"
        assert mask_secret_tail("abcd") == "****"

    def test_normal_value_shows_last_4(self):
        assert mask_secret_tail("cs_abcdefghij1234567890") == "...7890"

    def test_8_char_boundary_no_prefix_leak(self):
        assert mask_secret_tail("cs_12345") == "...2345"
        assert "cs_" not in mask_secret_tail("cs_12345")


class TestMaskSensitive:
    def test_masks_api_key(self):
        cfg = {"api_key": "cs_abcdefghij1234567890"}
        masked = mask_sensitive(cfg)
        assert masked["api_key"] == "...7890"

    def test_short_key_masked_fully(self):
        cfg = {"api_key": "ab"}
        masked = mask_sensitive(cfg)
        assert masked["api_key"] == "****"

    def test_medium_key_only_last_four_visible(self):
        cfg = {"api_key": "short"}
        masked = mask_sensitive(cfg)
        assert masked["api_key"] == "...hort"

    def test_empty_key_not_masked(self):
        cfg = {"api_key": ""}
        masked = mask_sensitive(cfg)
        assert masked["api_key"] == ""

    def test_masks_encryption_key(self):
        cfg = {"encryption_key": "a" * 64}
        masked = mask_sensitive(cfg)
        assert masked["encryption_key"] == "...aaaa"
        assert "a" * 64 not in str(masked)

    def test_non_sensitive_preserved(self):
        cfg = {"base_url": "https://api.certisigma.ch", "source": "test"}
        masked = mask_sensitive(cfg)
        assert masked["base_url"] == "https://api.certisigma.ch"

    def test_nested_masking(self):
        cfg = {"scan": {"include": ["*.pdf"]}}
        masked = mask_sensitive(cfg)
        assert masked["scan"]["include"] == ["*.pdf"]


class TestFindProjectConfig:
    def test_finds_config_in_current_dir(self, tmp_path):
        config_file = tmp_path / ".census.toml"
        config_file.write_text('api_key = "test"')
        result = _find_project_config(tmp_path)
        assert result == config_file

    def test_finds_config_in_parent(self, tmp_path):
        config_file = tmp_path / ".census.toml"
        config_file.write_text('api_key = "test"')
        child = tmp_path / "sub" / "dir"
        child.mkdir(parents=True)
        result = _find_project_config(child)
        assert result == config_file

    def test_returns_none_when_not_found(self, tmp_path):
        result = _find_project_config(tmp_path)
        assert result is None


class TestLoadConfig:
    def test_returns_defaults_when_no_files(self, tmp_path):
        cfg = load_config(project_dir=tmp_path, skip_user=True)
        assert cfg["api_key"] == ""
        assert cfg["base_url"] == "https://api.certisigma.ch"

    def test_project_config_overrides_defaults(self, tmp_path):
        config_file = tmp_path / ".census.toml"
        config_file.write_text('source = "my-project"\n')
        with patch("certisigma_census.config._find_project_config", return_value=config_file):
            cfg = load_config(skip_user=True)
        assert cfg["source"] == "my-project"

    def test_user_config_loaded(self, tmp_path):
        user_config = tmp_path / "config.toml"
        user_config.write_text('base_url = "https://custom.api.ch"\n')
        with patch("certisigma_census.config._user_config_path", return_value=user_config):
            cfg = load_config(skip_project=True)
        assert cfg["base_url"] == "https://custom.api.ch"

    def test_project_overrides_user(self, tmp_path):
        user_config = tmp_path / "user.toml"
        user_config.write_text('source = "user-source"\n')
        project_config = tmp_path / ".census.toml"
        project_config.write_text('source = "project-source"\n')
        with patch("certisigma_census.config._user_config_path", return_value=user_config), \
             patch("certisigma_census.config._find_project_config", return_value=project_config):
            cfg = load_config()
        assert cfg["source"] == "project-source"


class TestInitConfig:
    def test_creates_template(self, tmp_path):
        path = tmp_path / "sub" / "config.toml"
        init_config(path)
        assert path.exists()
        content = path.read_text()
        assert "CertiSigma Census" in content
        assert "api_key" in content

    def test_creates_parent_dirs(self, tmp_path):
        path = tmp_path / "deep" / "nested" / "config.toml"
        init_config(path)
        assert path.exists()


class TestLoadConfigIsolation:
    def test_returned_config_does_not_share_references_with_defaults(self, tmp_path):
        """load_config() must return an independent copy — no aliasing of nested dicts."""
        from certisigma_census.config import _DEFAULT_CONFIG

        cfg = load_config(project_dir=tmp_path, skip_user=True)
        cfg["scan"]["include"].append("*.log")
        assert _DEFAULT_CONFIG["scan"]["include"] == []


class TestResolveEncryptionKey:
    def test_explicit_key_wins(self):
        from certisigma_census.config import resolve_encryption_key
        assert resolve_encryption_key("ab" * 32) == "ab" * 32

    def test_from_config(self, tmp_path):
        from certisigma_census.config import resolve_encryption_key
        cfg_file = tmp_path / ".certisigma-census.toml"
        cfg_file.write_text(f'encryption_key = "{"cc" * 32}"\n')
        with patch("certisigma_census.config._find_project_config", return_value=cfg_file):
            key = resolve_encryption_key()
        assert key == "cc" * 32

    def test_env_var_overrides_config(self, monkeypatch, tmp_path):
        from certisigma_census.config import resolve_encryption_key
        env_key = "dd" * 32
        monkeypatch.setenv("CENSUS_ENCRYPTION_KEY", env_key)
        with patch("certisigma_census.config._find_project_config", return_value=None):
            with patch("certisigma_census.config._user_config_path", return_value=tmp_path / "nope.toml"):
                assert resolve_encryption_key() == env_key

    def test_explicit_overrides_env(self, monkeypatch):
        from certisigma_census.config import resolve_encryption_key
        monkeypatch.setenv("CENSUS_ENCRYPTION_KEY", "ee" * 32)
        assert resolve_encryption_key("ff" * 32) == "ff" * 32

    def test_missing_key_raises(self, tmp_path):
        from certisigma_census.config import resolve_encryption_key
        with patch("certisigma_census.config._find_project_config", return_value=None):
            with patch("certisigma_census.config._user_config_path", return_value=tmp_path / "nope.toml"):
                with pytest.raises(ValueError, match="No encryption key"):
                    resolve_encryption_key()


class TestLoadTomlEdgeCases:
    def test_oversized_config_ignored(self, tmp_path):
        big = tmp_path / "big.toml"
        big.write_bytes(b"a = 1\n" * 200_000)
        from certisigma_census.config import _load_toml, MAX_CONFIG_FILE_SIZE
        assert big.stat().st_size > MAX_CONFIG_FILE_SIZE
        result = _load_toml(big)
        assert result == {}

    def test_invalid_toml_returns_empty(self, tmp_path):
        bad = tmp_path / "bad.toml"
        bad.write_text("[invalid toml\n")
        from certisigma_census.config import _load_toml
        result = _load_toml(bad)
        assert result == {}


class TestInitConfigErrors:
    def test_existing_file_raises(self, tmp_path):
        path = tmp_path / "config.toml"
        path.write_text("existing")
        with pytest.raises(FileExistsError):
            init_config(path)


class TestInitConfigUnlinkWarning:
    def test_logs_when_partial_cleanup_fails(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
    ):
        """init_config must log a warning if partial config cleanup fails."""
        import certisigma_census.config as config_mod
        import os as _os

        path = tmp_path / "config.toml"
        orig_unlink = config_mod.os.unlink

        def block_unlink(p: str) -> None:
            if "config.toml" in str(p):
                raise OSError("simulated lock")
            orig_unlink(p)

        orig_fdopen = config_mod.os.fdopen

        def fail_fdopen(fd: int, *args, **kwargs):
            _os.close(fd)
            raise OSError("simulated fdopen failure")

        monkeypatch.setattr(config_mod.os, "fdopen", fail_fdopen)
        monkeypatch.setattr(config_mod.os, "unlink", block_unlink)

        with caplog.at_level(logging.WARNING, logger="certisigma_census.config"):
            with pytest.raises(OSError, match="simulated fdopen failure"):
                init_config(path)

        assert "Cannot remove partial config file" in caplog.text


class TestConfigPaths:
    def test_returns_user_and_project(self):
        paths = config_paths()
        assert "user" in paths
        assert "project" in paths
