# pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-arguments,too-many-return-statements
"""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 main
from abicheck.compat.cli import _apply_strict, _filter_source_only

# ---------------------------------------------------------------------------
# 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 -v1 / -v2 to override version labels
        abicc_r = _run_abicc_raw(
            v1_desc, v2_desc,
            extra_args=["-v1", "10.0", "-v2", "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"],
        )

        # ABICC 2.3 keeps pure additions compatible even under -strict (rc=0).
        # abicheck now matches this behaviour: pure additions stay COMPATIBLE
        # even in strict-mode=full.
        assert abicc_r.returncode == abicheck_r.returncode

    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);",
        # ABICC 2.3 keeps additions compatible even in -strict mode.
        "c", 0,
    ),
    (
        "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"],
    )

    # Known divergence: strict+addition semantics differ today.
    # 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"],
        )

        # ABICC 2.3 reports this case as source-compatible (warning-level, rc=0).
        # abicheck now matches: widening return-type changes (int→long) are
        # source-compatible in -source mode.
        assert abicc_r.returncode == 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"))
        # NOTE: int→long changes the mangled name (_Z7processi → _Z7processl).
        # abicheck detects this as func_removed + func_added (ELF-level symbol identity),
        # whereas ABICC additionally emits a high-level "func_params_changed" by matching
        # demangled names via castxml header analysis (TODO: implement header-based symbol
        # identity matching to produce func_params_changed in this case).
        kinds = {c["kind"] for c in data["changes"]}
        param_changes = "func_params_changed" in kinds
        assert param_changes or {"func_removed", "func_added"} <= kinds, (
            "abicheck should detect param type change as breaking "
            "(via func_params_changed or func_removed+func_added mangled-name change)"
        )

    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_pure_addition_stays_compatible(self):
        """Pure additions stay COMPATIBLE even under strict (ABICC parity)."""
        result = self._result(Verdict.COMPATIBLE, [ChangeKind.FUNC_ADDED])
        promoted = _apply_strict(result)
        assert promoted.verdict == Verdict.COMPATIBLE

    def test_strict_promotes_non_addition_compatible_to_breaking(self):
        """Non-addition COMPATIBLE → BREAKING in strict mode."""
        result = self._result(Verdict.COMPATIBLE, [ChangeKind.FUNC_REMOVED_ELF_ONLY])
        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
