"""Tests for the federation gate floor (``mgf.fed._pipeline``) — PAPER-121/122."""

from __future__ import annotations

import pytest

from mgf.fed._pipeline import (
    DEFAULT_REQUIRED_COMMANDS,
    command_lines,
    coverage_patch_gap,
    has_subprocess_patch,
    missing_commands,
    pipeline_missing_exempt,
    required_commands,
    runs_supervisor_under_coverage,
)

# A pipeline in the canonical shape: the two lint gates + the AG-03 trailer gate.
_GOOD = """\
steps:
  - name: lint
    image: python:3.12-slim
    commands:
      - pip install --quiet "ruff>=0.4,<0.16"
      - ruff check src tests
      - ruff format --check src tests

  - name: commit-trailers
    image: python:3.12-slim
    commands:
      - python scripts/check_commit_trailers.py

  - name: test-3.12
    image: python:3.12-slim
    commands:
      - coverage run --source=src/mgf -m mgf.test_supervisor --tier 0-2
      - coverage combine
      - coverage report --fail-under=80
"""

# The real-world PAPER-121 shape: every required step NAME is present, but the
# `ruff format --check` gate was never added to the bespoke pipeline's commands.
_NAMES_OK_COMMANDS_ROTTED = """\
steps:
  - name: lint
    image: python:3.12-slim
    commands:
      - ruff check src tests
      # - ruff format --check src tests   <- commented out is NOT a gate

  - name: commit-trailers
    commands:
      - python scripts/check_commit_trailers.py
"""


class TestCommandLines:
    def test_extracts_only_commands_not_step_names(self) -> None:
        lines = command_lines(_GOOD)
        assert "ruff check src tests" in lines
        assert "ruff format --check src tests" in lines
        assert "python scripts/check_commit_trailers.py" in lines
        # a `- name: lint` sequence item is a key, not a command
        assert not any(line.startswith("name:") for line in lines)
        assert not any("image:" in line for line in lines)

    def test_comments_are_not_commands(self) -> None:
        lines = command_lines(_NAMES_OK_COMMANDS_ROTTED)
        assert not any("format --check" in line for line in lines)

    def test_sequence_at_key_indent(self) -> None:
        text = "steps:\n  - name: lint\n    commands:\n    - ruff check .\n  - name: b\n"
        assert command_lines(text) == ["ruff check ."]

    def test_block_scalar_and_inline_flow_sequence(self) -> None:
        block = (
            "steps:\n  - name: lint\n    commands: |\n"
            "      ruff check .\n      ruff format --check .\n"
        )
        assert command_lines(block) == ["ruff check .", "ruff format --check ."]
        flow = "steps:\n  - name: lint\n    commands: ['ruff check .', 'pytest -q']\n"
        assert command_lines(flow) == ["ruff check .", "pytest -q"]

    def test_entrypoint_inline_scalar(self) -> None:
        assert command_lines("steps:\n  - name: x\n    entrypoint: /bin/sh -c true\n") == [
            "/bin/sh -c true"
        ]


class TestRequiredCommands:
    def test_defaults_when_policy_lacks_the_key(self) -> None:
        # The key belongs in mgf-standard's policy; until it lands, the gate floor
        # is still enforced from the built-in default (PAPER-121).
        assert required_commands({}) == DEFAULT_REQUIRED_COMMANDS
        assert "ruff format --check" in DEFAULT_REQUIRED_COMMANDS

    def test_policy_key_overrides_the_default(self) -> None:
        assert required_commands({"required_commands": ["pytest -q"]}) == ("pytest -q",)

    def test_empty_or_wrong_typed_key_falls_back(self) -> None:
        assert required_commands({"required_commands": []}) == DEFAULT_REQUIRED_COMMANDS
        assert required_commands({"required_commands": "ruff check"}) == DEFAULT_REQUIRED_COMMANDS


class TestMissingCommands:
    def test_canonical_pipeline_runs_every_gate(self) -> None:
        assert missing_commands(_GOOD, DEFAULT_REQUIRED_COMMANDS) == []

    def test_step_names_present_but_command_missing(self) -> None:
        # THE bug: the step is still called `lint`, the gate inside it is gone.
        assert missing_commands(_NAMES_OK_COMMANDS_ROTTED, DEFAULT_REQUIRED_COMMANDS) == [
            "ruff format --check"
        ]

    def test_bespoke_shape_is_free_but_honest(self) -> None:
        bespoke = (
            "steps:\n"
            "  - name: zig-build-and-test\n"
            "    commands:\n"
            "      - zig build test\n"
            "  - name: py-gates\n"
            "    commands:\n"
            "      - uv run ruff check .\n"
            "      - uv run ruff format --check .\n"
            "      - uv run python scripts/check_commit_trailers.py\n"
        )
        assert missing_commands(bespoke, DEFAULT_REQUIRED_COMMANDS) == []

    def test_no_commands_block_falls_back_to_raw_lines(self) -> None:
        # A hand-rolled pipeline with no recognisable `commands:` block must not be
        # reported as missing every gate just because the shape is unusual.
        raw = "pipeline:\n  lint:\n    script: ruff check src tests\n"
        assert missing_commands(raw, ("ruff check",)) == []
        assert missing_commands(raw, ("ruff format --check",)) == ["ruff format --check"]


class TestCoverageCoupling:
    def test_detects_supervisor_under_coverage(self) -> None:
        assert runs_supervisor_under_coverage(_GOOD)
        assert not runs_supervisor_under_coverage(_NAMES_OK_COMMANDS_ROTTED)
        # plain pytest, or the supervisor WITHOUT coverage, is not the coupling
        assert not runs_supervisor_under_coverage(
            "steps:\n  - name: t\n    commands:\n      - python -m mgf.test_supervisor --tier 0\n"
        )

    def test_has_subprocess_patch(self) -> None:
        assert has_subprocess_patch({"tool": {"coverage": {"run": {"patch": ["subprocess"]}}}})
        assert not has_subprocess_patch({"tool": {"coverage": {"run": {"source": ["src"]}}}})
        assert not has_subprocess_patch({"tool": {"coverage": {}}})
        assert not has_subprocess_patch({})

    def test_gap_fires_only_on_the_real_pair(self) -> None:
        pyproject_bad = {"project": {"name": "x"}}
        pyproject_good = {
            "project": {"name": "x"},
            "tool": {"coverage": {"run": {"patch": ["subprocess"]}}},
        }
        assert coverage_patch_gap(_GOOD, pyproject_bad)  # green suite, red build
        assert not coverage_patch_gap(_GOOD, pyproject_good)
        assert not coverage_patch_gap(_NAMES_OK_COMMANDS_ROTTED, pyproject_bad)

    def test_unreadable_pyproject_never_invents_a_failure(self) -> None:
        assert not coverage_patch_gap(_GOOD, {})


@pytest.mark.parametrize(
    ("policy", "expected"),
    [
        ({}, ()),
        ({"exempt": {}}, ()),
        # [exempt].woodpecker excuses generation-currency, NOT the pipeline itself.
        ({"exempt": {"woodpecker": ["mgf-hrb"]}}, ()),
        ({"exempt": {"no_pipeline": ["mgf-docs"]}}, ("mgf-docs",)),
    ],
)
def test_pipeline_missing_exempt(policy: dict[str, object], expected: tuple[str, ...]) -> None:
    assert pipeline_missing_exempt(policy) == expected
