"""Tests for config_loader — central configuration hub.

Covers:
- Schema accepts all new sections
- Config loader resolves built-in defaults when no file is present
- Config loader parses every new section from YAML data
- Environment variable overrides still work
- Deep merge preserves nested config
- Layered precedence: explicit overrides > config file values > defaults
"""

from __future__ import annotations

import json
import textwrap
from pathlib import Path

import pytest

from abi_control_core.sdk.config_loader import (
    ABIConfig,
    DiagnosticsConfig,
    ExecutionConfig,
    FleetScannerConfig,
    GradeThresholds,
    PromotionConfig,
    QualityConfig,
    QualityWeights,
    VerificationConfig,
    VisualVerificationConfig,
    _deep_merge,
    _parse_config,
    _validate_config,
    load_config,
)


# ---------------------------------------------------------------------------
# Schema validation
# ---------------------------------------------------------------------------

SCHEMA_PATH = Path(__file__).resolve().parent.parent / "schemas" / "abi_config.schema.json"


@pytest.fixture
def schema() -> dict:
    return json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))


def test_schema_file_exists():
    assert SCHEMA_PATH.exists(), "abi_config.schema.json must exist"


def test_schema_has_new_sections(schema: dict):
    props = schema["properties"]
    for section in [
        "execution",
        "visual_verification",
        "fleet_scanner",
        "verification",
        "quality",
        "diagnostics",
        "promotion",
    ]:
        assert section in props, f"Schema missing section: {section}"


def test_schema_execution_properties(schema: dict):
    exec_props = schema["properties"]["execution"]["properties"]
    assert "default_max_retries" in exec_props
    assert "max_verify_retries" in exec_props
    assert "timeout_per_invocation_seconds" in exec_props


def test_schema_quality_nested_structure(schema: dict):
    quality_props = schema["properties"]["quality"]["properties"]
    assert "weights" in quality_props
    assert "grade_thresholds" in quality_props
    weight_props = quality_props["weights"]["properties"]
    assert "test_coverage" in weight_props
    assert "doc_completeness" in weight_props


def test_schema_diagnostics_properties(schema: dict):
    diag_props = schema["properties"]["diagnostics"]["properties"]
    for key in ["build_output_chars", "verify_output_chars", "doom_loop_diff_chars", "max_findings_in_retry_prompt"]:
        assert key in diag_props


def test_schema_validates_minimal_config():
    """Schema validation passes for a minimal valid config."""
    data = {"version": "1.0"}
    # Should not raise
    _validate_config(data)


def test_schema_validates_full_config():
    """Schema validation passes for config with all new sections."""
    data = {
        "version": "1.0",
        "execution": {"default_max_retries": 5, "max_verify_retries": 2, "timeout_per_invocation_seconds": 300},
        "visual_verification": {"enabled": True, "base_url": "http://localhost:8080"},
        "fleet_scanner": {"doc_max_age_days": 60},
        "verification": {"mode": "full", "specialist_timeout_seconds": 180},
        "quality": {
            "weights": {"test_coverage": 0.5, "doc_completeness": 0.2, "known_issues": 0.1, "governance": 0.2},
            "grade_thresholds": {"A": 95, "B": 85, "C": 75, "D": 65},
        },
        "diagnostics": {"build_output_chars": 3000, "verify_output_chars": 4000},
        "promotion": {"enforcement_enabled": True},
    }
    _validate_config(data)


# ---------------------------------------------------------------------------
# Built-in defaults (no config file)
# ---------------------------------------------------------------------------


def test_defaults_no_config():
    """Parsing an empty dict yields all built-in defaults."""
    cfg = _parse_config({})
    assert isinstance(cfg, ABIConfig)

    # Execution
    assert cfg.execution.default_max_retries == 3
    assert cfg.execution.max_verify_retries == 1
    assert cfg.execution.timeout_per_invocation_seconds == 600

    # Visual verification
    assert cfg.visual_verification.enabled is False
    assert cfg.visual_verification.base_url == "http://localhost:3000"
    assert cfg.visual_verification.healthcheck_timeout_seconds == 30
    assert cfg.visual_verification.capture_timeout_seconds == 120
    assert cfg.visual_verification.page_timeout_ms == 30000

    # Fleet scanner
    assert cfg.fleet_scanner.doc_max_age_days == 90
    assert "governance_manifest.json" in cfg.fleet_scanner.required_governance_files

    # Verification
    assert cfg.verification.mode == "full"
    assert cfg.verification.specialist_timeout_seconds == 120
    assert "correctness" in cfg.verification.specialists

    # Quality
    assert cfg.quality.weights.test_coverage == 0.4
    assert cfg.quality.weights.doc_completeness == 0.2
    assert cfg.quality.grade_thresholds.A == 90
    assert cfg.quality.grade_thresholds.D == 60

    # Diagnostics
    assert cfg.diagnostics.build_output_chars == 2000
    assert cfg.diagnostics.verify_output_chars == 3000
    assert cfg.diagnostics.doom_loop_diff_chars == 5000
    assert cfg.diagnostics.max_findings_in_retry_prompt == 5

    # Promotion
    assert cfg.promotion.enforcement_enabled is False


# ---------------------------------------------------------------------------
# Parsing from config data
# ---------------------------------------------------------------------------


def test_parse_execution_overrides():
    data = {"execution": {"default_max_retries": 5, "timeout_per_invocation_seconds": 300}}
    cfg = _parse_config(data)
    assert cfg.execution.default_max_retries == 5
    assert cfg.execution.timeout_per_invocation_seconds == 300
    # Unspecified field keeps default
    assert cfg.execution.max_verify_retries == 1


def test_parse_visual_verification():
    data = {
        "visual_verification": {
            "enabled": True,
            "base_url": "http://localhost:8080",
            "page_timeout_ms": 60000,
        }
    }
    cfg = _parse_config(data)
    assert cfg.visual_verification.enabled is True
    assert cfg.visual_verification.base_url == "http://localhost:8080"
    assert cfg.visual_verification.page_timeout_ms == 60000
    # Defaults preserved for unspecified
    assert cfg.visual_verification.capture_timeout_seconds == 120


def test_parse_fleet_scanner():
    data = {"fleet_scanner": {"doc_max_age_days": 45, "required_governance_files": ["manifest.json"]}}
    cfg = _parse_config(data)
    assert cfg.fleet_scanner.doc_max_age_days == 45
    assert cfg.fleet_scanner.required_governance_files == ["manifest.json"]


def test_parse_verification_specialist_timeout():
    data = {"verification": {"specialist_timeout_seconds": 240}}
    cfg = _parse_config(data)
    assert cfg.verification.specialist_timeout_seconds == 240
    # Defaults
    assert cfg.verification.mode == "full"


def test_parse_quality_weights_and_thresholds():
    data = {
        "quality": {
            "weights": {"test_coverage": 0.5, "governance": 0.3},
            "grade_thresholds": {"A": 95, "B": 85},
        }
    }
    cfg = _parse_config(data)
    assert cfg.quality.weights.test_coverage == 0.5
    assert cfg.quality.weights.governance == 0.3
    # Unspecified weights keep default
    assert cfg.quality.weights.doc_completeness == 0.2
    assert cfg.quality.grade_thresholds.A == 95
    assert cfg.quality.grade_thresholds.B == 85
    # Unspecified thresholds keep default
    assert cfg.quality.grade_thresholds.C == 70


def test_parse_diagnostics():
    data = {"diagnostics": {"build_output_chars": 4000, "max_findings_in_retry_prompt": 10}}
    cfg = _parse_config(data)
    assert cfg.diagnostics.build_output_chars == 4000
    assert cfg.diagnostics.max_findings_in_retry_prompt == 10
    # Default
    assert cfg.diagnostics.verify_output_chars == 3000


def test_parse_promotion():
    data = {"promotion": {"enforcement_enabled": True}}
    cfg = _parse_config(data)
    assert cfg.promotion.enforcement_enabled is True


# ---------------------------------------------------------------------------
# Deep merge
# ---------------------------------------------------------------------------


def test_deep_merge_basic():
    base = {"a": 1, "b": {"x": 10, "y": 20}}
    overlay = {"b": {"y": 99, "z": 30}, "c": 3}
    result = _deep_merge(base, overlay)
    assert result == {"a": 1, "b": {"x": 10, "y": 99, "z": 30}, "c": 3}


def test_deep_merge_overlay_wins_on_scalar():
    base = {"execution": {"default_max_retries": 3}}
    overlay = {"execution": {"default_max_retries": 5}}
    result = _deep_merge(base, overlay)
    assert result["execution"]["default_max_retries"] == 5


# ---------------------------------------------------------------------------
# Layered precedence via load_config
# ---------------------------------------------------------------------------


def test_load_config_from_file(tmp_path: Path):
    """Config from file overrides built-in defaults."""
    cfg_file = tmp_path / "abi.config.yaml"
    cfg_file.write_text(textwrap.dedent("""\
        version: "1.0"
        execution:
          default_max_retries: 7
        diagnostics:
          build_output_chars: 5000
        promotion:
          enforcement_enabled: true
    """))
    cfg = load_config(str(cfg_file))
    assert cfg.execution.default_max_retries == 7
    assert cfg.diagnostics.build_output_chars == 5000
    assert cfg.promotion.enforcement_enabled is True
    # Defaults for unspecified sections
    assert cfg.visual_verification.enabled is False


def test_load_config_overrides_beat_file(tmp_path: Path):
    """Explicit overrides take highest priority."""
    cfg_file = tmp_path / "abi.config.yaml"
    cfg_file.write_text(textwrap.dedent("""\
        version: "1.0"
        execution:
          default_max_retries: 7
    """))
    cfg = load_config(str(cfg_file), overrides={"execution": {"default_max_retries": 10}})
    assert cfg.execution.default_max_retries == 10


def test_load_config_env_overrides(tmp_path: Path, monkeypatch):
    """Environment variables override config file values."""
    cfg_file = tmp_path / "abi.config.yaml"
    cfg_file.write_text('version: "1.0"\nbudget:\n  max_total_calls: 5\n')
    monkeypatch.setenv("ABI_BUDGET_MAX_CALLS", "99")
    cfg = load_config(str(cfg_file))
    assert cfg.budget.max_total_calls == 99


def test_load_config_nonexistent_raises():
    with pytest.raises(FileNotFoundError):
        load_config("/nonexistent/abi.config.yaml")


# ---------------------------------------------------------------------------
# Dataclass defaults are correct types
# ---------------------------------------------------------------------------


def test_execution_config_defaults():
    ec = ExecutionConfig()
    assert isinstance(ec.default_max_retries, int)
    assert isinstance(ec.max_verify_retries, int)
    assert isinstance(ec.timeout_per_invocation_seconds, int)


def test_visual_verification_config_defaults():
    vv = VisualVerificationConfig()
    assert isinstance(vv.routes, list)
    assert isinstance(vv.viewports, list)
    assert len(vv.routes) > 0
    assert len(vv.viewports) > 0


def test_quality_weights_sum_to_one():
    w = QualityWeights()
    total = w.test_coverage + w.doc_completeness + w.known_issues + w.governance
    assert abs(total - 1.0) < 1e-9, f"Quality weights should sum to 1.0, got {total}"


def test_grade_thresholds_descending():
    t = GradeThresholds()
    assert t.A > t.B > t.C > t.D, "Thresholds must be in descending order"
