"""Extended ABICC parity tests: CLI compatibility, output format parity,
and strict-mode case-level equivalence.

Three test dimensions beyond basic verdict parity (test_abicc_parity.py):

1. CLI compatibility — abicheck compat accepts the same flags, aliases,
   and option styles that abi-compliance-checker does.
2. Output format parity — both tools produce reports with matching structure
   (exit codes, report metadata, change counts, section layout).
3. Strict/compact mode parity — in strict mode, both tools detect the same
   level of cases and agree on verdict promotion semantics.

Requires: abi-compliance-checker, gcc/g++, castxml.
"""
from __future__ import annotations

import json
import re
import shutil
import subprocess
import textwrap
from pathlib import Path

import pytest

from abicheck.checker import Change, ChangeKind, DiffResult, Verdict
from abicheck.cli import (
    _apply_strict,
    _filter_source_only,
    main,
)

# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------

def _require_tool(name: str) -> None:
    if shutil.which(name) is None:
        pytest.skip(f"{name} not found in PATH")


def _compile_so(src: str, out: Path, lang: str) -> None:
    ext = ".c" if lang == "c" else ".cpp"
    src_file = out.with_suffix(ext)
    src_file.write_text(textwrap.dedent(src).strip(), encoding="utf-8")
    compiler = "gcc" if lang == "c" else "g++"
    cmd = [compiler, "-shared", "-fPIC", "-g", "-fvisibility=default",
           "-o", str(out), str(src_file)]
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    if r.returncode != 0:
        pytest.fail(f"Compilation failed: {r.stderr[:200]}")


def _write_header(content: str, path: Path) -> Path:
    path.write_text(textwrap.dedent(content).strip(), encoding="utf-8")
    return path


def _write_abicc_descriptor(
    version: str,
    lib_path: Path,
    header_path: Path | None,
    desc_path: Path,
) -> None:
    lines = ["<descriptor>", f"  <version>{version}</version>"]
    if header_path is not None and header_path.exists():
        lines.append(f"  <headers>{header_path}</headers>")
    lines.append(f"  <libs>{lib_path}</libs>")
    lines.append("</descriptor>")
    desc_path.write_text("\n".join(lines), encoding="utf-8")


def _run_abicc_raw(
    old_desc: Path,
    new_desc: Path,
    extra_args: list[str] | None = None,
    report_path: Path | None = None,
    timeout: int = 60,
) -> subprocess.CompletedProcess[str]:
    """Run abi-compliance-checker and return the raw CompletedProcess."""
    if report_path is None:
        report_path = old_desc.parent / "abicc_report.html"
    cmd = [
        "abi-compliance-checker",
        "-lib", "libtest",
        "-old", str(old_desc),
        "-new", str(new_desc),
        "-report-path", str(report_path),
    ]
    if extra_args:
        cmd.extend(extra_args)
    try:
        return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        pytest.fail("ABICC timed out")


def _parse_abicc_report_metadata(report_path: Path) -> dict[str, str]:
    """Parse the structured HTML comment ABICC embeds at the top of reports.

    Returns a dict of field→value from the comment:
      <!-- kind:binary;verdict:compatible;affected:0;added:0;removed:0;... -->
    """
    if not report_path.exists():
        return {}
    text = report_path.read_text(encoding="utf-8", errors="replace")
    m = re.search(r"<!--\s*([^>]+?)\s*-->", text)
    if not m:
        return {}
    parts = m.group(1).split(";")
    result = {}
    for part in parts:
        if ":" in part:
            k, v = part.split(":", 1)
            result[k.strip()] = v.strip()
    return result


def _build_test_libs(
    tmp_path: Path,
    src_v1: str,
    src_v2: str,
    hdr_v1: str,
    hdr_v2: str,
    lang: str = "c",
    name: str = "test",
) -> tuple[Path, Path, Path, Path, Path, Path]:
    """Compile libs and write descriptors.

    Returns: (v1_so, v2_so, v1_desc, v2_desc, v1_hdr, v2_hdr)
    """
    _require_tool("gcc" if lang == "c" else "g++")

    v1_so = tmp_path / f"lib{name}_v1.so"
    v2_so = tmp_path / f"lib{name}_v2.so"
    _compile_so(src_v1, v1_so, lang)
    _compile_so(src_v2, v2_so, lang)

    v1_hdr = _write_header(hdr_v1, tmp_path / f"{name}_v1.h")
    v2_hdr = _write_header(hdr_v2, tmp_path / f"{name}_v2.h")

    v1_desc = tmp_path / "old.xml"
    v2_desc = tmp_path / "new.xml"
    _write_abicc_descriptor("1.0", v1_so, v1_hdr, v1_desc)
    _write_abicc_descriptor("2.0", v2_so, v2_hdr, v2_desc)

    return v1_so, v2_so, v1_desc, v2_desc, v1_hdr, v2_hdr


def _run_abicheck_compat(
    old_desc: Path,
    new_desc: Path,
    extra_args: list[str] | None = None,
    report_path: Path | None = None,
    fmt: str = "json",
) -> subprocess.CompletedProcess[str]:
    """Run abicheck compat as a subprocess (for exit-code testing)."""
    import sys
    cmd = [
        sys.executable, "-m", "abicheck.cli",
        "compat",
        "-lib", "libtest",
        "-old", str(old_desc),
        "-new", str(new_desc),
        "-report-format", fmt,
    ]
    if report_path:
        cmd.extend(["-report-path", str(report_path)])
    if extra_args:
        cmd.extend(extra_args)
    return subprocess.run(cmd, capture_output=True, text=True, timeout=60)


# ============================================================================
# 1. CLI COMPATIBILITY TESTS
# ============================================================================

@pytest.mark.abicc
class TestCliCompatibility:
    """Verify abicheck compat CLI mirrors abi-compliance-checker's interface."""

    # ── Flag acceptance: all major ABICC flags must be recognized ──────────

    def test_abicc_accepts_strict_flag(self, tmp_path):
        """abicheck compat must accept -strict flag (ABICC compatibility)."""
        from click.testing import CliRunner
        runner = CliRunner()
        # Check that abicheck compat --help mentions -strict / -s
        result = runner.invoke(main, ["compat", "check", "--help"])
        assert result.exit_code == 0, f"compat --help failed: {result.output}"
        assert "-strict" in result.output or "-s" in result.output, (
            "abicheck compat --help does not list -strict / -s flag"
        )

    def test_abicheck_compat_accepts_all_abicc_flags(self):
        """abicheck compat --help lists all ABICC-equivalent flags."""
        from click.testing import CliRunner
        runner = CliRunner()
        result = runner.invoke(main, ["compat", "check", "--help"])
        assert result.exit_code == 0

        required_flags = [
            "-lib", "-old", "-new", "-d1", "-d2",
            "-report-path", "-report-format",
            "-s", "-strict",
            "-source", "-src", "-api",
            "-binary", "-bin", "-abi",
            "-v1", "-vnum1", "-v2", "-vnum2",
            "-stdout",
            "-skip-symbols", "-skip-types",
            "-headers-only",
            "-show-retval",
            "-title",
        ]
        for flag in required_flags:
            assert flag in result.output, (
                f"Flag '{flag}' not found in compat --help output"
            )

    # ── Exit code parity ──────────────────────────────────────────────────

    def test_exit_code_0_compatible_parity(self, tmp_path):
        """Both tools exit 0 for compatible (no-change) libraries."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src = "int add(int a, int b) { return a + b; }"
        hdr = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src, src, hdr, hdr,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc)
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc, report_path=abicheck_rpt,
        )

        assert abicc_r.returncode == 0, f"ABICC expected rc=0, got {abicc_r.returncode}"
        assert abicheck_r.returncode == 0, f"abicheck expected rc=0, got {abicheck_r.returncode}"

    def test_exit_code_1_breaking_parity(self, tmp_path):
        """Both tools exit 1 for breaking changes (function removed)."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc)
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc, report_path=abicheck_rpt,
        )

        assert abicc_r.returncode == 1, f"ABICC expected rc=1, got {abicc_r.returncode}"
        assert abicheck_r.returncode == 1, f"abicheck expected rc=1, got {abicheck_r.returncode}"

    def test_exit_code_0_addition_only_parity(self, tmp_path):
        """Both tools exit 0 for compatible additions (new function)."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }"
        src_v2 = "int add(int a, int b) { return a + b; }\nint mul(int a, int b) { return a*b; }"
        hdr_v1 = "int add(int a, int b);"
        hdr_v2 = "int add(int a, int b);\nint mul(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc)
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc, report_path=abicheck_rpt,
        )

        assert abicc_r.returncode == 0, f"ABICC expected rc=0, got {abicc_r.returncode}"
        assert abicheck_r.returncode == 0, f"abicheck expected rc=0, got {abicheck_r.returncode}"

    # ── Descriptor format parity ──────────────────────────────────────────

    def test_abicc_descriptor_format_accepted(self, tmp_path):
        """abicheck compat accepts identical ABICC XML descriptors."""
        _require_tool("castxml")

        src = "int add(int a, int b) { return a + b; }"
        hdr = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src, src, hdr, hdr,
        )

        # Verify descriptor is valid ABICC XML
        desc_text = v1_desc.read_text()
        assert "<version>" in desc_text
        assert "<libs>" in desc_text

        abicheck_rpt = tmp_path / "abicheck_report.json"
        r = _run_abicheck_compat(
            v1_desc, v2_desc, report_path=abicheck_rpt,
        )
        assert r.returncode == 0, f"abicheck rejected valid ABICC descriptor: {r.stderr}"

    def test_version_override_flags_v1_v2(self, tmp_path):
        """Both tools accept version override flags."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src = "int add(int a, int b) { return a + b; }"
        hdr = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src, src, hdr, hdr,
        )

        # ABICC uses -v1num / -v2num
        abicc_r = _run_abicc_raw(
            v1_desc, v2_desc,
            extra_args=["-v1num", "10.0", "-v2num", "20.0"],
        )
        # abicheck uses -v1 / -v2
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc,
            extra_args=["-v1", "10.0", "-v2", "20.0"],
            report_path=abicheck_rpt,
        )

        # Both should succeed (rc=0 for no-change)
        assert abicc_r.returncode == 0, f"ABICC rc={abicc_r.returncode}"
        assert abicheck_r.returncode == 0, f"abicheck rc={abicheck_r.returncode}"

        # abicheck should embed overridden version labels in the report
        if abicheck_rpt.exists():
            report = json.loads(abicheck_rpt.read_text(encoding="utf-8"))
            assert report["old_version"] == "10.0"
            assert report["new_version"] == "20.0"


# ============================================================================
# 2. OUTPUT FORMAT PARITY TESTS
# ============================================================================

@pytest.mark.abicc
class TestOutputFormatParity:
    """Compare report structure and content between the two tools."""

    def test_report_change_counts_match_for_removal(self, tmp_path):
        """Function removal: both tools report exactly 1 removed symbol."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        # ABICC report
        abicc_report = tmp_path / "abicc_report.html"
        _run_abicc_raw(v1_desc, v2_desc, report_path=abicc_report)
        abicc_meta = _parse_abicc_report_metadata(abicc_report)

        # abicheck report (JSON for easy parsing)
        abicheck_report = tmp_path / "abicheck_report.json"
        _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)

        abicheck_data = json.loads(abicheck_report.read_text(encoding="utf-8"))

        # ABICC removed count
        abicc_removed = int(abicc_meta.get("removed", "0"))
        # abicheck removed count from changes
        abicheck_removed = sum(
            1 for c in abicheck_data["changes"]
            if "removed" in c["kind"]
        )

        # Both should detect exactly 1 removal (sub was removed)
        assert abicc_removed == 1, f"ABICC removed={abicc_removed}, expected 1"
        assert abicheck_removed == 1, f"abicheck removed={abicheck_removed}, expected 1"
        assert abicc_removed == abicheck_removed, (
            f"Removal count mismatch: ABICC={abicc_removed}, abicheck={abicheck_removed}"
        )

    def test_report_verdict_string_matches(self, tmp_path):
        """Verdict strings should be semantically equivalent."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicc_report = tmp_path / "abicc_report.html"
        _run_abicc_raw(v1_desc, v2_desc, report_path=abicc_report)
        abicc_meta = _parse_abicc_report_metadata(abicc_report)

        abicheck_report = tmp_path / "abicheck_report.json"
        _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)
        abicheck_data = json.loads(abicheck_report.read_text(encoding="utf-8"))

        # ABICC uses "incompatible"/"compatible" in its metadata comment
        abicc_verdict = abicc_meta.get("verdict", "").lower()
        abicheck_verdict = abicheck_data["verdict"]

        assert abicc_verdict, (
            f"ABICC report metadata missing or empty verdict "
            f"(metadata={abicc_meta!r})"
        )

        if abicc_verdict == "incompatible":
            assert abicheck_verdict == "BREAKING", (
                f"ABICC says incompatible, abicheck says {abicheck_verdict}"
            )
        elif abicc_verdict == "compatible":
            assert abicheck_verdict in ("COMPATIBLE", "NO_CHANGE"), (
                f"ABICC says compatible, abicheck says {abicheck_verdict}"
            )
        else:
            pytest.fail(
                f"Unexpected ABICC verdict '{abicc_verdict}' "
                f"(abicheck verdict={abicheck_verdict}, metadata={abicc_meta!r})"
            )

    def test_report_json_structure_complete(self, tmp_path):
        """abicheck JSON report has all required fields for ABICC parity."""
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicheck_report = tmp_path / "abicheck_report.json"
        _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)

        data = json.loads(abicheck_report.read_text(encoding="utf-8"))

        # Required top-level fields (ABICC-equivalent)
        assert "library" in data
        assert "old_version" in data
        assert "new_version" in data
        assert "verdict" in data
        assert "changes" in data
        assert "summary" in data

        # Summary must have ABICC-equivalent counts
        summary = data["summary"]
        assert "breaking" in summary
        assert "total_changes" in summary

        # Each change must have kind, symbol, description
        for change in data["changes"]:
            assert "kind" in change
            assert "symbol" in change
            assert "description" in change

    def test_report_html_contains_abicc_sections(self, tmp_path):
        """HTML report has ABICC-equivalent sections: verdict, summary, changes."""
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicheck_report = tmp_path / "abicheck_report.html"
        _run_abicheck_compat(
            v1_desc, v2_desc, report_path=abicheck_report, fmt="html",
        )

        html_text = abicheck_report.read_text(encoding="utf-8")

        # ABICC-equivalent structure
        assert "Verdict" in html_text, "Missing verdict section"
        assert "Binary Compatibility" in html_text, "Missing BC% metric"
        assert "Change Summary" in html_text or "Summary" in html_text, "Missing summary"
        assert "Removed" in html_text, "Missing removed section"

    def test_report_html_parity_structure(self, tmp_path):
        """Both tools' HTML reports have equivalent structural elements."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        # ABICC report
        abicc_report = tmp_path / "abicc_report.html"
        _run_abicc_raw(v1_desc, v2_desc, report_path=abicc_report)

        # abicheck report
        abicheck_report = tmp_path / "abicheck_report.html"
        _run_abicheck_compat(
            v1_desc, v2_desc, report_path=abicheck_report, fmt="html",
        )

        abicc_html = abicc_report.read_text(encoding="utf-8") if abicc_report.exists() else ""
        abicheck_html = abicheck_report.read_text(encoding="utf-8") if abicheck_report.exists() else ""

        # Both should be valid HTML
        assert "<html" in abicc_html.lower(), "ABICC didn't produce HTML"
        assert "<html" in abicheck_html.lower(), "abicheck didn't produce HTML"

        # Both should contain a compatibility metric
        assert "%" in abicc_html, "ABICC report missing compatibility %"
        assert "%" in abicheck_html, "abicheck report missing compatibility %"

    def test_markdown_report_has_abicc_equivalent_sections(self, tmp_path):
        """Markdown report contains ABICC-equivalent sections."""
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicheck_report = tmp_path / "abicheck_report.md"
        _run_abicheck_compat(
            v1_desc, v2_desc, report_path=abicheck_report, fmt="md",
        )

        md_text = abicheck_report.read_text(encoding="utf-8")

        # ABICC-equivalent report structure in markdown
        assert "ABI Report" in md_text, "Missing ABI Report header"
        assert "Verdict" in md_text, "Missing verdict"
        assert "Breaking" in md_text, "Missing breaking changes section"
        assert "Old version" in md_text, "Missing old version"
        assert "New version" in md_text, "Missing new version"

    def test_stdout_flag_outputs_report(self, tmp_path):
        """abicheck -stdout prints report content to stdout, matching ABICC behavior."""
        _require_tool("castxml")

        src = "int add(int a, int b) { return a + b; }"
        hdr = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src, src, hdr, hdr,
        )

        abicheck_report = tmp_path / "abicheck_report.json"
        r = _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_report,
            extra_args=["-stdout"],
        )

        # -stdout should cause report to appear on stdout
        assert r.stdout.strip(), "-stdout flag did not produce stdout output"
        # The stdout should be valid JSON (since we requested json format)
        data = json.loads(r.stdout)
        assert "verdict" in data


# ============================================================================
# 3. STRICT MODE PARITY TESTS
# ============================================================================

@pytest.mark.abicc
class TestStrictModeParity:
    """Verify -strict semantics match between abicheck and ABICC.

    ABICC -strict: any change at all (even additions) = exit 1.
    abicheck -strict: COMPATIBLE and API_BREAK promoted to BREAKING (exit 1).
    """

    def test_strict_no_change_exit_0(self, tmp_path):
        """Both tools: -strict + no changes = exit 0."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src = "int add(int a, int b) { return a + b; }"
        hdr = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src, src, hdr, hdr,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc, extra_args=["-strict"])
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_rpt,
            extra_args=["-s"],
        )

        assert abicc_r.returncode == 0, f"ABICC strict + no_change: rc={abicc_r.returncode}"
        assert abicheck_r.returncode == 0, f"abicheck strict + no_change: rc={abicheck_r.returncode}"

    def test_strict_addition_exit_code_parity(self, tmp_path):
        """Strict mode with additions: both tools promote to error.

        ABICC -strict: additions → exit 1 (any change = error).
        abicheck -s: COMPATIBLE → promoted to BREAKING → exit 1.
        """
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }"
        src_v2 = "int add(int a, int b) { return a + b; }\nint mul(int a, int b) { return a*b; }"
        hdr_v1 = "int add(int a, int b);"
        hdr_v2 = "int add(int a, int b);\nint mul(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc, extra_args=["-strict"])
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_rpt,
            extra_args=["-s"],
        )

        # Both should exit non-zero for additions in strict mode
        # ABICC uses exit 1 for strict violations
        assert abicc_r.returncode != 0, f"ABICC strict + addition: rc={abicc_r.returncode} (expected non-0)"
        assert abicheck_r.returncode != 0, f"abicheck strict + addition: rc={abicheck_r.returncode} (expected non-0)"

        # Specifically, both should exit 1
        assert abicc_r.returncode == 1, f"ABICC strict + addition: rc={abicc_r.returncode} (expected 1)"
        assert abicheck_r.returncode == 1, f"abicheck strict + addition: rc={abicheck_r.returncode} (expected 1)"

    def test_strict_breaking_exit_1_both(self, tmp_path):
        """Strict mode + breaking: both tools exit 1."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc, extra_args=["-strict"])
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_rpt,
            extra_args=["-s"],
        )

        assert abicc_r.returncode == 1, f"ABICC strict + breaking: rc={abicc_r.returncode}"
        assert abicheck_r.returncode == 1, f"abicheck strict + breaking: rc={abicheck_r.returncode}"

    def test_strict_verdict_json_shows_breaking(self, tmp_path):
        """In strict mode, abicheck JSON verdict is BREAKING for additions."""
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }"
        src_v2 = "int add(int a, int b) { return a + b; }\nint mul(int a, int b) { return a*b; }"
        hdr_v1 = "int add(int a, int b);"
        hdr_v2 = "int add(int a, int b);\nint mul(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicheck_rpt = tmp_path / "abicheck_report.json"
        _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_rpt,
            extra_args=["-s"],
        )

        data = json.loads(abicheck_rpt.read_text(encoding="utf-8"))
        assert data["verdict"] == "BREAKING", (
            f"Strict mode should promote COMPATIBLE to BREAKING, got {data['verdict']}"
        )


# ============================================================================
# 4. STRICT + COMPACT MODE CASE-LEVEL PARITY
# ============================================================================

# Cases that both tools should detect at the same level in strict mode
STRICT_PARITY_CASES: list[tuple[str, str, str, str, str, str, int]] = [
    # (name, src_v1, src_v2, hdr_v1, hdr_v2, lang, expected_exit_strict)
    (
        "strict_no_change",
        "int x(void) { return 1; }",
        "int x(void) { return 1; }",
        "int x(void);",
        "int x(void);",
        "c", 0,
    ),
    (
        "strict_addition",
        "int x(void) { return 1; }",
        "int x(void) { return 1; }\nint y(void) { return 2; }",
        "int x(void);",
        "int x(void);\nint y(void);",
        "c", 1,
    ),
    (
        "strict_removal",
        "int x(void) { return 1; }\nint y(void) { return 2; }",
        "int x(void) { return 1; }",
        "int x(void);\nint y(void);",
        "int x(void);",
        "c", 1,
    ),
    (
        "strict_return_type",
        "int  get(void) { return 42; }",
        "long get(void) { return 42; }",
        "int  get(void);",
        "long get(void);",
        "c", 1,
    ),
    (
        "strict_param_type",
        "void set(int  x) { (void)x; }",
        "void set(long x) { (void)x; }",
        "void set(int  x);",
        "void set(long x);",
        "c", 1,
    ),
    (
        "strict_enum_value",
        "typedef enum { A=0, B=1 } E;\nE get_e(void) { return A; }",
        "typedef enum { A=0, B=10 } E;\nE get_e(void) { return A; }",
        "typedef enum { A=0, B=1 } E;\nE get_e(void);",
        "typedef enum { A=0, B=10 } E;\nE get_e(void);",
        "c", 1,
    ),
]


@pytest.mark.abicc
@pytest.mark.parametrize(
    "name,src_v1,src_v2,hdr_v1,hdr_v2,lang,expected_exit",
    STRICT_PARITY_CASES,
    ids=[c[0] for c in STRICT_PARITY_CASES],
)
def test_strict_case_level_parity(
    name: str,
    src_v1: str, src_v2: str,
    hdr_v1: str, hdr_v2: str,
    lang: str,
    expected_exit: int,
    tmp_path: Path,
) -> None:
    """Both tools agree on exit code in -strict mode for each case."""
    _require_tool("abi-compliance-checker")
    _require_tool("castxml")
    _require_tool("gcc" if lang == "c" else "g++")

    _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
        tmp_path, src_v1, src_v2, hdr_v1, hdr_v2, lang=lang, name=name,
    )

    abicc_r = _run_abicc_raw(v1_desc, v2_desc, extra_args=["-strict"])
    abicheck_rpt = tmp_path / f"{name}_report.json"
    abicheck_r = _run_abicheck_compat(
        v1_desc, v2_desc,
        report_path=abicheck_rpt,
        extra_args=["-s"],
    )

    # Both tools should return the expected exit code
    assert abicc_r.returncode == expected_exit, (
        f"[{name}] ABICC strict: rc={abicc_r.returncode}, expected {expected_exit}"
    )
    assert abicheck_r.returncode == expected_exit, (
        f"[{name}] abicheck strict: rc={abicheck_r.returncode}, expected {expected_exit}"
    )
    # The key assertion: both tools agree on the exact return code
    assert abicc_r.returncode == abicheck_r.returncode, (
        f"[{name}] STRICT PARITY BROKEN: "
        f"ABICC rc={abicc_r.returncode}, abicheck rc={abicheck_r.returncode}"
    )


# ============================================================================
# 5. SOURCE-MODE PARITY TESTS
# ============================================================================

class TestSourceModeUnit:
    """Unit tests for source mode filtering (no external tools required)."""

    def test_source_mode_ignores_elf_only_changes(self):
        """_filter_source_only removes binary-only changes."""
        changes = [
            Change(kind=ChangeKind.SONAME_CHANGED, symbol="libtest.so",
                   description="soname changed"),
            Change(kind=ChangeKind.FUNC_PARAMS_CHANGED, symbol="foo",
                   description="param type changed"),
        ]
        result = DiffResult(
            old_version="1.0", new_version="2.0",
            library="libtest.so", changes=changes,
            verdict=Verdict.BREAKING,
        )
        filtered = _filter_source_only(result)
        kinds = {c.kind for c in filtered.changes}
        assert ChangeKind.SONAME_CHANGED not in kinds
        assert ChangeKind.FUNC_PARAMS_CHANGED in kinds
        assert filtered.verdict == Verdict.BREAKING


@pytest.mark.abicc
class TestSourceModeParity:
    """Source (-source) mode: both tools should agree on verdict."""

    def test_source_mode_exit_code_parity(self, tmp_path):
        """With -source, both tools agree on verdict for source-level break."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int  get(void) { return 42; }"
        src_v2 = "long get(void) { return 42; }"
        hdr_v1 = "int  get(void);"
        hdr_v2 = "long get(void);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicc_r = _run_abicc_raw(
            v1_desc, v2_desc, extra_args=["-source"],
        )
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_rpt,
            extra_args=["-source"],
        )

        # Return type change is a source-level break. Both should detect it.
        assert abicc_r.returncode != 0, f"ABICC source mode: rc={abicc_r.returncode}"
        assert abicheck_r.returncode != 0, f"abicheck source mode: rc={abicheck_r.returncode}"

    def test_source_mode_no_change_both_pass(self, tmp_path):
        """With -source and no changes, both tools exit 0."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src = "int add(int a, int b) { return a + b; }"
        hdr = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src, src, hdr, hdr,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc, extra_args=["-source"])
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_rpt,
            extra_args=["-source"],
        )

        assert abicc_r.returncode == 0
        assert abicheck_r.returncode == 0


# ============================================================================
# 6. COMPREHENSIVE STRICT-MODE CASE DETECTION LEVEL
# ============================================================================

@pytest.mark.abicc
class TestStrictCompactCaseLevel:
    """Verify both tools detect the same number and type of changes
    in strict-compact scenarios (single isolated changes)."""

    def test_single_func_removal_change_count(self, tmp_path):
        """Exactly 1 breaking change detected by both tools."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int a(void) { return 1; }\nint b(void) { return 2; }"
        src_v2 = "int a(void) { return 1; }"
        hdr_v1 = "int a(void);\nint b(void);"
        hdr_v2 = "int a(void);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        # ABICC
        abicc_report = tmp_path / "abicc_report.html"
        _run_abicc_raw(v1_desc, v2_desc, report_path=abicc_report)
        abicc_meta = _parse_abicc_report_metadata(abicc_report)
        abicc_removed = int(abicc_meta.get("removed", "0"))

        # abicheck
        abicheck_report = tmp_path / "abicheck_report.json"
        _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)
        data = json.loads(abicheck_report.read_text(encoding="utf-8"))
        abicheck_removed = sum(
            1 for c in data["changes"]
            if c["kind"] == "func_removed"
        )

        # Both should detect exactly 1 removal
        assert abicc_removed == 1, f"ABICC: {abicc_removed} removed"
        assert abicheck_removed == 1, f"abicheck: {abicheck_removed} removed"

    def test_multiple_func_removal_change_count(self, tmp_path):
        """Multiple removals: same count from both tools."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = (
            "int a(void) { return 1; }\n"
            "int b(void) { return 2; }\n"
            "int c(void) { return 3; }"
        )
        src_v2 = "int a(void) { return 1; }"
        hdr_v1 = "int a(void);\nint b(void);\nint c(void);"
        hdr_v2 = "int a(void);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        # ABICC
        abicc_report = tmp_path / "abicc_report.html"
        _run_abicc_raw(v1_desc, v2_desc, report_path=abicc_report)
        abicc_meta = _parse_abicc_report_metadata(abicc_report)
        abicc_removed = int(abicc_meta.get("removed", "0"))

        # abicheck
        abicheck_report = tmp_path / "abicheck_report.json"
        _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)
        data = json.loads(abicheck_report.read_text(encoding="utf-8"))
        abicheck_removed = sum(
            1 for c in data["changes"]
            if c["kind"] == "func_removed"
        )

        # Both should detect 2 removals (b and c)
        assert abicc_removed == 2, f"ABICC: {abicc_removed} removed"
        assert abicheck_removed == 2, f"abicheck: {abicheck_removed} removed"

    def test_addition_count_parity(self, tmp_path):
        """Addition count matches between tools."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int a(void) { return 1; }"
        src_v2 = (
            "int a(void) { return 1; }\n"
            "int b(void) { return 2; }\n"
            "int c(void) { return 3; }"
        )
        hdr_v1 = "int a(void);"
        hdr_v2 = "int a(void);\nint b(void);\nint c(void);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        # ABICC
        abicc_report = tmp_path / "abicc_report.html"
        _run_abicc_raw(v1_desc, v2_desc, report_path=abicc_report)
        abicc_meta = _parse_abicc_report_metadata(abicc_report)
        abicc_added = int(abicc_meta.get("added", "0"))

        # abicheck
        abicheck_report = tmp_path / "abicheck_report.json"
        _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)
        data = json.loads(abicheck_report.read_text(encoding="utf-8"))
        abicheck_added = sum(
            1 for c in data["changes"]
            if c["kind"] == "func_added"
        )

        # Both should detect 2 additions (b and c)
        assert abicc_added == 2, f"ABICC: {abicc_added} added"
        assert abicheck_added == 2, f"abicheck: {abicheck_added} added"

    def test_return_type_change_detected_by_both(self, tmp_path):
        """Return type change: both tools detect it as a changed function."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int  get(void) { return 42; }"
        src_v2 = "long get(void) { return 42; }"
        hdr_v1 = "int  get(void);"
        hdr_v2 = "long get(void);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        # ABICC
        abicc_report = tmp_path / "abicc_report.html"
        abicc_r = _run_abicc_raw(v1_desc, v2_desc, report_path=abicc_report)
        abicc_meta = _parse_abicc_report_metadata(abicc_report)

        # abicheck
        abicheck_report = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)
        data = json.loads(abicheck_report.read_text(encoding="utf-8"))

        # Both should exit 1 (breaking)
        assert abicc_r.returncode == 1
        assert abicheck_r.returncode == 1

        # abicheck should have a func_return_changed change
        return_changes = [c for c in data["changes"] if c["kind"] == "func_return_changed"]
        assert len(return_changes) >= 1, "abicheck didn't detect return type change"

        # ABICC should report affected > 0
        abicc_affected = int(abicc_meta.get("affected", "0"))
        assert abicc_affected >= 1, f"ABICC didn't detect the change: affected={abicc_affected}"

    def test_param_type_change_detected_by_both(self, tmp_path):
        """Parameter type change: both detect as breaking."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "void process(int  x) { (void)x; }"
        src_v2 = "void process(long x) { (void)x; }"
        hdr_v1 = "void process(int  x);"
        hdr_v2 = "void process(long x);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc)
        abicheck_report = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)

        assert abicc_r.returncode == 1, "ABICC should detect param type change"
        assert abicheck_r.returncode == 1, "abicheck should detect param type change"

        data = json.loads(abicheck_report.read_text(encoding="utf-8"))
        param_changes = [c for c in data["changes"] if c["kind"] == "func_params_changed"]
        assert len(param_changes) >= 1

    def test_enum_value_change_detected_by_both(self, tmp_path):
        """Enum value change: both detect as breaking."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "typedef enum { A=0, B=1, C=2 } MyEnum;\nMyEnum get_e(void) { return A; }"
        src_v2 = "typedef enum { A=0, B=5, C=2 } MyEnum;\nMyEnum get_e(void) { return A; }"
        hdr_v1 = "typedef enum { A=0, B=1, C=2 } MyEnum;\nMyEnum get_e(void);"
        hdr_v2 = "typedef enum { A=0, B=5, C=2 } MyEnum;\nMyEnum get_e(void);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        abicc_r = _run_abicc_raw(v1_desc, v2_desc)
        abicheck_report = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(v1_desc, v2_desc, report_path=abicheck_report)

        # Both should detect this
        assert abicc_r.returncode == 1, "ABICC should detect enum value change"
        assert abicheck_r.returncode == 1, "abicheck should detect enum value change"

        data = json.loads(abicheck_report.read_text(encoding="utf-8"))
        enum_changes = [c for c in data["changes"] if "enum" in c["kind"]]
        assert len(enum_changes) >= 1, "abicheck didn't emit enum change"


# ============================================================================
# 7. SKIP-SYMBOLS / SKIP-TYPES PARITY
# ============================================================================

@pytest.mark.abicc
class TestSkipFilterParity:
    """Both tools should agree on verdict when skip-symbols filters are applied."""

    def test_skip_symbols_suppresses_correctly(self, tmp_path):
        """Skipping the removed symbol should make both tools pass."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }"
        src_v2 = "int add(int a, int b) { return a + b; }"
        hdr_v1 = "int add(int a, int b);\nint sub(int a, int b);"
        hdr_v2 = "int add(int a, int b);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        # Create skip file with "sub"
        skip_file = tmp_path / "skip.txt"
        skip_file.write_text("sub\n", encoding="utf-8")

        # ABICC with -skip-symbols
        abicc_r = _run_abicc_raw(
            v1_desc, v2_desc,
            extra_args=["-skip-symbols", str(skip_file)],
        )

        # abicheck with -skip-symbols
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_rpt,
            extra_args=["-skip-symbols", str(skip_file)],
        )

        # Both should exit 0 (the only breaking change was sub, which is skipped)
        assert abicc_r.returncode == 0, (
            f"ABICC with skip-symbols: rc={abicc_r.returncode}\n{abicc_r.stderr}"
        )
        assert abicheck_r.returncode == 0, (
            f"abicheck with skip-symbols: rc={abicheck_r.returncode}\n{abicheck_r.stderr}"
        )

    def test_skip_symbols_partial_still_breaks(self, tmp_path):
        """Skipping one of two removed symbols still results in breaking."""
        _require_tool("abi-compliance-checker")
        _require_tool("castxml")

        src_v1 = (
            "int a(void) { return 1; }\n"
            "int b(void) { return 2; }\n"
            "int c(void) { return 3; }"
        )
        src_v2 = "int a(void) { return 1; }"
        hdr_v1 = "int a(void);\nint b(void);\nint c(void);"
        hdr_v2 = "int a(void);"
        _, _, v1_desc, v2_desc, _, _ = _build_test_libs(
            tmp_path, src_v1, src_v2, hdr_v1, hdr_v2,
        )

        # Skip only "b", "c" is still removed
        skip_file = tmp_path / "skip.txt"
        skip_file.write_text("b\n", encoding="utf-8")

        abicc_r = _run_abicc_raw(
            v1_desc, v2_desc,
            extra_args=["-skip-symbols", str(skip_file)],
        )
        abicheck_rpt = tmp_path / "abicheck_report.json"
        abicheck_r = _run_abicheck_compat(
            v1_desc, v2_desc,
            report_path=abicheck_rpt,
            extra_args=["-skip-symbols", str(skip_file)],
        )

        # Both should still detect breaking (c is still removed)
        assert abicc_r.returncode == 1, f"ABICC: rc={abicc_r.returncode}"
        assert abicheck_r.returncode == 1, f"abicheck: rc={abicheck_r.returncode}"


# ============================================================================
# 8. UNIT TESTS: STRICT VERDICT PROMOTION LOGIC
# ============================================================================

class TestStrictVerdictPromotion:
    """Unit tests for strict-mode verdict promotion logic (no external tools).

    Uses the production _apply_strict() function to verify promotion semantics.
    """

    def _result(self, verdict: Verdict, kinds: list[ChangeKind] | None = None) -> DiffResult:
        changes = [
            Change(kind=k, symbol=f"_sym_{i}", description=k.value)
            for i, k in enumerate(kinds or [])
        ]
        return DiffResult(
            old_version="1.0", new_version="2.0",
            library="libtest.so.1",
            changes=changes,
            verdict=verdict,
        )

    def test_strict_promotes_compatible_to_breaking(self):
        """COMPATIBLE → BREAKING in strict mode."""
        result = self._result(Verdict.COMPATIBLE, [ChangeKind.FUNC_ADDED])
        promoted = _apply_strict(result)
        assert promoted.verdict == Verdict.BREAKING

    def test_strict_promotes_source_break_to_breaking(self):
        """API_BREAK → BREAKING in strict mode."""
        result = self._result(Verdict.API_BREAK, [ChangeKind.FUNC_PARAMS_CHANGED])
        promoted = _apply_strict(result)
        assert promoted.verdict == Verdict.BREAKING

    def test_strict_no_change_stays_no_change(self):
        """NO_CHANGE is not promoted — even in strict mode."""
        result = self._result(Verdict.NO_CHANGE)
        promoted = _apply_strict(result)
        assert promoted.verdict == Verdict.NO_CHANGE

    def test_strict_breaking_stays_breaking(self):
        """BREAKING stays BREAKING in strict mode (no double-promotion)."""
        result = self._result(Verdict.BREAKING, [ChangeKind.FUNC_REMOVED])
        promoted = _apply_strict(result)
        assert promoted.verdict == Verdict.BREAKING

    def test_source_filter_then_strict_promotion(self):
        """Source filter + strict: binary-only changes removed, then verdict promoted."""
        # Result with only binary-only change → after source filter = NO_CHANGE
        result = self._result(Verdict.BREAKING, [ChangeKind.SONAME_CHANGED])
        filtered = _filter_source_only(result)
        assert filtered.verdict == Verdict.NO_CHANGE
        # Strict on NO_CHANGE should stay NO_CHANGE
        promoted = _apply_strict(filtered)
        assert promoted.verdict == Verdict.NO_CHANGE

        # Source filter + FUNC_PARAMS_CHANGED → BREAKING after source filter
        result2 = self._result(
            Verdict.BREAKING,
            [ChangeKind.SONAME_CHANGED, ChangeKind.FUNC_PARAMS_CHANGED],
        )
        filtered2 = _filter_source_only(result2)
        assert filtered2.verdict == Verdict.BREAKING
        assert len(filtered2.changes) == 1
        assert filtered2.changes[0].kind == ChangeKind.FUNC_PARAMS_CHANGED
        # Strict on BREAKING should stay BREAKING
        promoted2 = _apply_strict(filtered2)
        assert promoted2.verdict == Verdict.BREAKING
