"""AZ-271 — Config precedence tests (env > YAML > defaults).

Verifies the precedence rule for ≥3 keys at each layer plus the
multi-file YAML merge order (later wins) per epic AZ-246 AC-3. Tests
are hermetic: env is passed in via the loader's ``env`` argument and
YAML is materialised via ``tmp_path``.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from gps_denied_onboard.config import (
    Config,
    FdrConfig,
    LogConfig,
    RuntimeConfig,
    load_config,
)

REQUIRED_ENV: dict[str, str] = {
    "GPS_DENIED_FC_PROFILE": "ardupilot_plane",
    "GPS_DENIED_TIER": "1",
    "DB_URL": "postgresql://localhost:5432/test",
    "CAMERA_CALIBRATION_PATH": "/tmp/cal.yaml",
    "LOG_LEVEL": "INFO",
    "LOG_SINK": "console",
    "INFERENCE_BACKEND": "pytorch_fp16",
    "FDR_PATH": "/tmp/fdr",
    "TILE_CACHE_PATH": "/tmp/tiles",
}


def _write_yaml(tmp_path: Path, name: str, content: str) -> Path:
    path = tmp_path / name
    path.write_text(content)
    return path


def _layer_msg(layer: str, key: str, expected: object, actual: object) -> str:
    """Standardised assertion message naming the precedence layer (AC-5)."""
    return (
        f"precedence layer {layer!r} for key {key!r}: "
        f"expected {expected!r} (from {layer}), got {actual!r}"
    )


# ---------------------------------------------------------------------------
# AC-1: env wins over YAML for ≥3 keys (LOG_LEVEL, FDR_QUEUE_SIZE, GPS_DENIED_TIER).


def test_ac1_env_wins_over_yaml_for_three_keys(tmp_path: Path) -> None:
    # Arrange
    yaml_path = _write_yaml(
        tmp_path,
        "base.yaml",
        """
log:
  level: WARN
fdr:
  queue_size: 8192
runtime:
  tier: 2
""",
    )
    env = dict(REQUIRED_ENV)
    env["LOG_LEVEL"] = "ERROR"
    env["FDR_QUEUE_SIZE"] = "16384"
    env["GPS_DENIED_TIER"] = "1"

    # Act
    config = load_config(env=env, paths=(yaml_path,))

    # Assert
    assert config.log.level == "ERROR", _layer_msg("env", "log.level", "ERROR", config.log.level)
    assert config.fdr.queue_size == 16384, _layer_msg(
        "env", "fdr.queue_size", 16384, config.fdr.queue_size
    )
    assert config.runtime.tier == 1, _layer_msg("env", "runtime.tier", 1, config.runtime.tier)


# ---------------------------------------------------------------------------
# AC-2: YAML wins over defaults for ≥3 keys.


def test_ac2_yaml_wins_over_defaults_for_three_keys(tmp_path: Path) -> None:
    # Arrange
    yaml_path = _write_yaml(
        tmp_path,
        "base.yaml",
        """
log:
  level: DEBUG
  sink: journald
fdr:
  queue_size: 2048
""",
    )
    env = dict(REQUIRED_ENV)
    # Remove any env keys that map to the YAML overrides — we want YAML > defaults
    # without env shadowing.
    for env_key in ("LOG_LEVEL", "LOG_SINK", "FDR_QUEUE_SIZE"):
        env.pop(env_key, None)
    # LOG_LEVEL is in REQUIRED_ENV but the loader's required-env gate would
    # complain; bypass with ``require_env=False`` to keep the test hermetic.

    # Act
    config = load_config(env=env, paths=(yaml_path,), require_env=False)

    # Assert
    log_defaults = LogConfig()
    fdr_defaults = FdrConfig()
    assert config.log.level == "DEBUG", _layer_msg("yaml", "log.level", "DEBUG", config.log.level)
    assert config.log.level != log_defaults.level
    assert config.log.sink == "journald", _layer_msg(
        "yaml", "log.sink", "journald", config.log.sink
    )
    assert config.log.sink != log_defaults.sink
    assert config.fdr.queue_size == 2048, _layer_msg(
        "yaml", "fdr.queue_size", 2048, config.fdr.queue_size
    )
    assert config.fdr.queue_size != fdr_defaults.queue_size


# ---------------------------------------------------------------------------
# AC-3: defaults apply for ≥3 keys when env + YAML omit them.


def test_ac3_defaults_apply_when_layers_silent() -> None:
    # Arrange — empty YAML, env_default-only-required-vars.
    env = dict(REQUIRED_ENV)
    # Strip three env keys so loader falls to defaults for those three.
    env.pop("LOG_LEVEL")
    env.pop("LOG_SINK")
    env.pop("FDR_QUEUE_SIZE", None)  # FDR_QUEUE_SIZE is not required

    # Act
    config = load_config(env=env, paths=(), require_env=False)

    # Assert
    log_defaults = LogConfig()
    fdr_defaults = FdrConfig()
    runtime_defaults = RuntimeConfig()
    assert config.log.level == log_defaults.level, _layer_msg(
        "default", "log.level", log_defaults.level, config.log.level
    )
    assert config.log.sink == log_defaults.sink, _layer_msg(
        "default", "log.sink", log_defaults.sink, config.log.sink
    )
    assert config.fdr.queue_size == fdr_defaults.queue_size, _layer_msg(
        "default", "fdr.queue_size", fdr_defaults.queue_size, config.fdr.queue_size
    )
    # Sanity: runtime defaults also intact for keys with NO env override.
    assert (
        config.runtime.inference_backend == runtime_defaults.inference_backend
        or env.get("INFERENCE_BACKEND") == config.runtime.inference_backend
    )


# ---------------------------------------------------------------------------
# AC-4: multi-file YAML — later wins.


def test_ac4_multi_file_yaml_later_wins(tmp_path: Path) -> None:
    # Arrange
    first = _write_yaml(
        tmp_path,
        "first.yaml",
        """
log:
  level: WARN
fdr:
  queue_size: 1024
""",
    )
    second = _write_yaml(
        tmp_path,
        "second.yaml",
        """
log:
  level: ERROR
fdr:
  queue_size: 8192
""",
    )
    env = dict(REQUIRED_ENV)
    env.pop("LOG_LEVEL")  # don't let env shadow the YAML precedence test

    # Act
    config = load_config(env=env, paths=(first, second), require_env=False)

    # Assert
    assert config.log.level == "ERROR", _layer_msg(
        "later-yaml", "log.level", "ERROR", config.log.level
    )
    assert config.fdr.queue_size == 8192, _layer_msg(
        "later-yaml", "fdr.queue_size", 8192, config.fdr.queue_size
    )


# ---------------------------------------------------------------------------
# AC-5: assertion message names the layer (verified by introspecting helper).


def test_ac5_failure_messages_name_the_layer() -> None:
    # Arrange
    msg = _layer_msg("env", "log.level", "ERROR", "INFO")

    # Assert — message contains the layer name, the key, and both values.
    assert "env" in msg
    assert "log.level" in msg
    assert "ERROR" in msg
    assert "INFO" in msg


# ---------------------------------------------------------------------------
# Smoke: load_config + compose_root integrate (regression guard).


def test_load_config_returns_frozen_config_dataclass() -> None:
    # Arrange
    env = dict(REQUIRED_ENV)

    # Act
    config = load_config(env=env, paths=())

    # Assert
    import dataclasses

    assert isinstance(config, Config)
    with pytest.raises(dataclasses.FrozenInstanceError):
        # frozen=True → cannot mutate
        config.log = LogConfig()  # type: ignore[misc]
