import pytest
import yaml
from pathlib import Path
from project_hub.core.config import Config, load_config, save_config


# ---------------------------------------------------------------------------
# load_config basics (existing tests)
# ---------------------------------------------------------------------------


def test_load_default_config(tmp_hub: Path):
    """Minimal config file: non-specified fields use defaults."""
    config_path = tmp_hub / ".project-hub" / "config.yaml"
    # tmp_hub already has a minimal config (just forges); verify defaults kick in
    cfg = load_config(config_path)
    assert cfg.pull_strategy == "merge"
    assert cfg.cache_ttl == 300
    assert cfg.http_port == 8400
    assert cfg.watch_mrs_mode == "mine"


def test_load_custom_config(tmp_hub: Path):
    """Custom values override defaults."""
    config_path = tmp_hub / ".project-hub" / "config.yaml"
    config_path.write_text(
        "pull:\n  strategy: rebase\ncache_ttl: 60\n"
    )
    cfg = load_config(config_path)
    assert cfg.pull_strategy == "rebase"
    assert cfg.cache_ttl == 60
    # non-specified fields stay at defaults
    assert cfg.http_port == 8400
    assert cfg.watch_mrs_mode == "mine"


def test_load_with_include_exclude(tmp_hub: Path):
    """include list and exclude list are parsed correctly."""
    config_path = tmp_hub / ".project-hub" / "config.yaml"
    config_path.write_text(
        "include:\n  - 'repo-*'\nexclude:\n  - repo-gamma\n"
    )
    cfg = load_config(config_path)
    assert cfg.include == ["repo-*"]
    assert cfg.exclude == ["repo-gamma"]


def test_missing_config_uses_defaults(tmp_path: Path):
    """Non-existent config file returns a Config with all defaults."""
    cfg = load_config(tmp_path / "does-not-exist.yaml")
    assert cfg.pull_strategy == "merge"
    assert cfg.cache_ttl == 300
    assert cfg.http_port == 8400
    assert cfg.watch_mrs_mode == "mine"
    assert cfg.include == []
    assert cfg.exclude == []
    assert cfg.forges == {}


def test_unknown_keys_ignored(tmp_hub: Path):
    """Config with unknown keys loads without error."""
    config_path = tmp_hub / ".project-hub" / "config.yaml"
    config_path.write_text("unknown_key: whatever\nanother_unknown: 42\n")
    cfg = load_config(config_path)
    assert cfg.pull_strategy == "merge"  # default preserved


def test_malformed_yaml_exits(tmp_hub: Path):
    """Invalid YAML raises SystemExit."""
    config_path = tmp_hub / ".project-hub" / "config.yaml"
    config_path.write_text("key: [unclosed\n")
    with pytest.raises(SystemExit):
        load_config(config_path)


# ---------------------------------------------------------------------------
# save_config + load_config roundtrip
# ---------------------------------------------------------------------------


def test_save_load_roundtrip_defaults(tmp_path: Path):
    """save_config then load_config with default Config produces identical values."""
    config_path = tmp_path / "config.yaml"
    original = Config()

    save_config(config_path, original)
    loaded = load_config(config_path)

    assert loaded.pull_strategy == original.pull_strategy
    assert loaded.cache_ttl == original.cache_ttl
    assert loaded.http_port == original.http_port
    assert loaded.watch_mrs_mode == original.watch_mrs_mode
    assert loaded.watch_pipelines_mode == original.watch_pipelines_mode
    assert loaded.watch_vulns_mode == original.watch_vulns_mode
    assert loaded.watch_vulns_min_severity == original.watch_vulns_min_severity
    assert loaded.watch_issues_mode == original.watch_issues_mode
    assert loaded.include == original.include
    assert loaded.exclude == original.exclude
    assert loaded.forges == original.forges


def test_save_load_roundtrip_all_fields(tmp_path: Path):
    """save_config then load_config preserves every field when all are set."""
    config_path = tmp_path / "config.yaml"
    original = Config(
        include=["pa-*"],
        exclude=["repo-gamma", "repo-delta"],
        forges={
            "gitlab.com": {"type": "gitlab"},
            "gitlab.internal.io": {"type": "gitlab"},
        },
        pull_strategy="rebase",
        watch_mrs_mode="all",
        watch_pipelines_mode="none",
        watch_vulns_mode="mine",
        watch_vulns_min_severity="critical",
        watch_issues_mode="all",
        cache_ttl=120,
        http_port=9000,
    )

    save_config(config_path, original)
    loaded = load_config(config_path)

    assert loaded.include == ["pa-*"]
    assert loaded.exclude == ["repo-gamma", "repo-delta"]
    assert loaded.forges == {"gitlab.com": {"type": "gitlab"}, "gitlab.internal.io": {"type": "gitlab"}}
    assert loaded.pull_strategy == "rebase"
    assert loaded.watch_mrs_mode == "all"
    assert loaded.watch_pipelines_mode == "none"
    assert loaded.watch_vulns_mode == "mine"
    assert loaded.watch_vulns_min_severity == "critical"
    assert loaded.watch_issues_mode == "all"
    assert loaded.cache_ttl == 120
    assert loaded.http_port == 9000


def test_save_load_roundtrip_no_include_no_exclude(tmp_path: Path):
    """When include and exclude are empty, they stay empty after roundtrip."""
    config_path = tmp_path / "config.yaml"
    original = Config(include=[], exclude=[])

    save_config(config_path, original)
    loaded = load_config(config_path)

    assert loaded.include == []
    assert loaded.exclude == []


# ---------------------------------------------------------------------------
# load_config edge cases
# ---------------------------------------------------------------------------


def test_load_empty_file_returns_defaults(tmp_path: Path):
    """An empty YAML file returns all defaults (yaml.safe_load returns None)."""
    config_path = tmp_path / "config.yaml"
    config_path.write_text("")

    cfg = load_config(config_path)

    assert cfg.pull_strategy == "merge"
    assert cfg.cache_ttl == 300
    assert cfg.http_port == 8400
    assert cfg.include == []
    assert cfg.exclude == []
    assert cfg.forges == {}


def test_load_unknown_keys_preserves_known(tmp_path: Path):
    """Unknown keys are ignored; known keys in the same file are loaded."""
    config_path = tmp_path / "config.yaml"
    config_path.write_text(
        "future_feature: enabled\n"
        "cache_ttl: 60\n"
        "http_port: 7777\n"
        "alien_setting:\n  nested: true\n"
    )

    cfg = load_config(config_path)

    assert cfg.cache_ttl == 60
    assert cfg.http_port == 7777
    # Defaults for non-specified keys
    assert cfg.pull_strategy == "merge"
    assert cfg.watch_mrs_mode == "mine"


def test_load_partial_watch_config(tmp_path: Path):
    """Partial watch block fills defaults for missing sub-keys."""
    config_path = tmp_path / "config.yaml"
    config_path.write_text(
        "watch:\n"
        "  mrs:\n"
        "    mode: all\n"
        "  # pipelines, vulns, issues omitted\n"
    )

    cfg = load_config(config_path)

    assert cfg.watch_mrs_mode == "all"
    assert cfg.watch_pipelines_mode == "mine"  # default
    assert cfg.watch_vulns_mode == "all"  # default
    assert cfg.watch_vulns_min_severity == "high"  # default
    assert cfg.watch_issues_mode == "mine"  # default


def test_load_null_watch_blocks(tmp_path: Path):
    """watch block with null sub-keys uses defaults."""
    config_path = tmp_path / "config.yaml"
    config_path.write_text(
        "watch:\n"
        "  mrs: null\n"
        "  pipelines: null\n"
    )

    cfg = load_config(config_path)

    assert cfg.watch_mrs_mode == "mine"
    assert cfg.watch_pipelines_mode == "mine"


def test_load_null_pull_block(tmp_path: Path):
    """pull block set to null uses default strategy."""
    config_path = tmp_path / "config.yaml"
    config_path.write_text("pull: null\n")

    cfg = load_config(config_path)

    assert cfg.pull_strategy == "merge"


# ---------------------------------------------------------------------------
# save_config atomicity
# ---------------------------------------------------------------------------


def test_save_writes_via_tmp_rename(tmp_path: Path):
    """save_config writes to .tmp first then renames (atomic write pattern)."""
    config_path = tmp_path / "config.yaml"
    config = Config()

    save_config(config_path, config)

    # After save, .tmp should NOT exist (it was renamed)
    assert not config_path.with_suffix(".tmp").exists()
    # The final file should exist and be valid YAML
    assert config_path.exists()
    data = yaml.safe_load(config_path.read_text())
    assert isinstance(data, dict)


def test_save_creates_parent_directory_not_required(tmp_path: Path):
    """save_config requires the parent directory to exist (it doesn't create it)."""
    config_path = tmp_path / "nested" / "deep" / "config.yaml"

    # save_config doesn't create parent dirs -- verify it raises
    with pytest.raises(FileNotFoundError):
        save_config(config_path, Config())


def test_save_overwrites_existing_file(tmp_path: Path):
    """save_config overwrites an existing config file."""
    config_path = tmp_path / "config.yaml"
    config_path.write_text("old: content\n")

    save_config(config_path, Config(cache_ttl=999))

    loaded = load_config(config_path)
    assert loaded.cache_ttl == 999


# ---------------------------------------------------------------------------
# Config field validation (default sanity)
# ---------------------------------------------------------------------------


def test_default_cache_ttl_positive():
    """Default cache_ttl is positive."""
    cfg = Config()
    assert cfg.cache_ttl > 0


def test_default_http_port_valid():
    """Default http_port is in a valid range."""
    cfg = Config()
    assert 1024 <= cfg.http_port <= 65535


def test_default_pull_strategy_known():
    """Default pull_strategy is a recognized value."""
    cfg = Config()
    assert cfg.pull_strategy in ("merge", "rebase")


def test_default_watch_modes_known():
    """Default watch modes are recognized values."""
    cfg = Config()
    valid_modes = {"mine", "all", "none"}
    assert cfg.watch_mrs_mode in valid_modes
    assert cfg.watch_pipelines_mode in valid_modes
    assert cfg.watch_vulns_mode in valid_modes
    assert cfg.watch_issues_mode in valid_modes


def test_default_vuln_severity_known():
    """Default vuln min severity is a recognized value."""
    cfg = Config()
    assert cfg.watch_vulns_min_severity in ("low", "medium", "high", "critical")
