"""Tests for immutable release-check verification and workflow contracts."""

from collections.abc import Sequence
import importlib.util
import json
from pathlib import Path
import re
import shutil
import subprocess
import sys
from types import SimpleNamespace
from typing import Any

import pytest

SCRIPT_PATH = Path(__file__).parents[1] / ".github" / "scripts" / "verify_release_checks.py"
SCRIPT_SPEC = importlib.util.spec_from_file_location("verify_release_checks", SCRIPT_PATH)
assert SCRIPT_SPEC is not None
assert SCRIPT_SPEC.loader is not None
verify = importlib.util.module_from_spec(SCRIPT_SPEC)
SCRIPT_SPEC.loader.exec_module(verify)

WORKFLOW_ROOT = Path(__file__).parents[1] / ".github" / "workflows"
REPOSITORY = "owner/repository"
REF = "release-validation/v1.0.6-123-1"
SHA = "a" * 40


def _workflow_text(name: str) -> str:
    """Return one workflow as text for structural contract checks.

    Args:
        name (str): Workflow filename.

    Returns:
        str: Workflow source text.
    """
    return (WORKFLOW_ROOT / name).read_text(encoding="utf-8")


def _step_containing(workflow: str, token: str) -> str:
    """Return the unique workflow step containing a semantic token.

    Args:
        workflow (str): Workflow source text.
        token (str): Stable command or expression that identifies the step.

    Returns:
        str: Matching workflow step source.
    """
    steps = re.split(r"(?m)(?=^      - )", workflow)
    matches = [step for step in steps if token in step]
    assert len(matches) == 1
    return matches[0]


def _normalized(value: str) -> str:
    """Collapse insignificant whitespace for shell contract assertions.

    Args:
        value (str): Source text to normalize.

    Returns:
        str: Whitespace-collapsed text.
    """
    return " ".join(value.split())


def test_dispatch_workflow_uses_expected_ref_sha_and_authoritative_run_id(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Send the candidate identity and accept only GitHub's returned run ID.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing the API helper.
    """
    calls: list[tuple[list[str], int | None]] = []

    def fake_api(arguments: Sequence[str], expected_status: int | None = None) -> dict[str, Any]:
        calls.append((list(arguments), expected_status))
        return {"workflow_run_id": 42}

    monkeypatch.setattr(verify, "github_api", fake_api)

    assert verify.dispatch_workflow(REPOSITORY, "validate.yml", REF, SHA) == 42
    arguments, status = calls[0]
    assert status == 200
    assert f"repos/{REPOSITORY}/actions/workflows/validate.yml/dispatches" in arguments
    run_details_index = arguments.index("return_run_details=true")
    assert arguments[run_details_index - 1] == "-F"
    assert f"ref={REF}" in arguments
    assert f"inputs[expected_sha]={SHA}" in arguments


@pytest.mark.parametrize("run_id", [None, 0, -1, True, "42"])
def test_dispatch_workflow_rejects_missing_or_invalid_run_id(
    monkeypatch: pytest.MonkeyPatch, run_id: object
) -> None:
    """Fail closed when the dispatch response is not an authoritative run ID.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing the API helper.
        run_id (object): Invalid response value.
    """
    monkeypatch.setattr(
        verify,
        "github_api",
        lambda _arguments, expected_status=None: {"workflow_run_id": run_id},
    )

    with pytest.raises(verify.GitHubCommandError):
        verify.dispatch_workflow(REPOSITORY, "validate.yml", REF, SHA)


def test_parse_required_checks_derives_workflows_in_first_seen_order() -> None:
    """Group required jobs while preserving workflow dispatch order."""
    checks = verify.parse_required_checks(
        [
            "pytest_check.yml::pytest and coverage report",
            "validate.yml::HACS Validation",
            "validate.yml::Hassfest Validation",
            "pytest_check.yml::pytest and coverage report",
        ]
    )

    assert list(checks) == ["pytest_check.yml", "validate.yml"]
    assert checks == {
        "pytest_check.yml": {"pytest and coverage report"},
        "validate.yml": {"HACS Validation", "Hassfest Validation"},
    }


def test_wait_for_workflow_requires_exact_identity_and_successful_jobs(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Verify the exact dispatched run, Actions suite, and exact required job.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing API and time helpers.
    """

    def fake_api(arguments: Sequence[str]) -> dict[str, Any]:
        endpoint = arguments[0]
        if endpoint.endswith("/actions/workflows/validate.yml"):
            return {"id": 7}
        if endpoint.endswith("/actions/runs/42"):
            return {
                "id": 42,
                "workflow_id": 7,
                "event": "workflow_dispatch",
                "head_branch": REF,
                "head_sha": SHA,
                "status": "completed",
                "conclusion": "success",
                "check_suite_id": 99,
            }
        if endpoint.endswith("/check-suites/99"):
            return {"head_sha": SHA, "app": {"slug": "github-actions"}}
        if endpoint.endswith("/actions/runs/42/jobs?per_page=100"):
            return {
                "total_count": 1,
                "jobs": [{"name": "HACS Validation", "conclusion": "success"}],
            }
        raise AssertionError(f"Unexpected GitHub API endpoint: {endpoint}")

    monkeypatch.setattr(verify, "github_api", fake_api)
    monkeypatch.setattr(verify.time, "monotonic", lambda: 0.0)

    assert (
        verify.wait_for_workflow(
            REPOSITORY,
            "validate.yml",
            REF,
            SHA,
            {"HACS Validation"},
            deadline=1.0,
            expected_run_id=42,
        )
        == 42
    )


def test_wait_for_workflow_retries_a_transient_run_not_found(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Retry a transient run lookup failure before verifying its completed checks.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing API and time helpers.
    """
    run_attempts = 0
    sleeps: list[int] = []

    def fake_api(arguments: Sequence[str]) -> dict[str, Any]:
        """Return endpoint-specific responses with one transient run miss.

        Args:
            arguments (Sequence[str]): GitHub API request arguments.

        Returns:
            dict[str, Any]: Response fixture for the requested endpoint.

        Raises:
            verify.GitHubCommandError: On the first workflow-run lookup.
            AssertionError: If the implementation requests an unexpected endpoint.
        """
        nonlocal run_attempts
        endpoint = arguments[0]
        if endpoint.endswith("/actions/workflows/validate.yml"):
            return {"id": 7}
        if endpoint.endswith("/actions/runs/42"):
            run_attempts += 1
            if run_attempts == 1:
                raise verify.GitHubCommandError("HTTP 404 Not Found")
            return {
                "id": 42,
                "workflow_id": 7,
                "event": "workflow_dispatch",
                "head_branch": REF,
                "head_sha": SHA,
                "status": "completed",
                "conclusion": "success",
                "check_suite_id": 99,
            }
        if endpoint.endswith("/check-suites/99"):
            return {"head_sha": SHA, "app": {"slug": "github-actions"}}
        if endpoint.endswith("/actions/runs/42/jobs?per_page=100"):
            return {
                "total_count": 1,
                "jobs": [{"name": "HACS Validation", "conclusion": "success"}],
            }
        raise AssertionError(f"Unexpected GitHub API endpoint: {endpoint}")

    monkeypatch.setattr(verify, "github_api", fake_api)
    monkeypatch.setattr(verify.time, "monotonic", lambda: 0.0)
    monkeypatch.setattr(verify.time, "sleep", sleeps.append)

    assert (
        verify.wait_for_workflow(
            REPOSITORY,
            "validate.yml",
            REF,
            SHA,
            {"HACS Validation"},
            deadline=1.0,
            expected_run_id=42,
        )
        == 42
    )
    assert any(delay > 0 for delay in sleeps)


@pytest.mark.parametrize(
    "run",
    [
        {"id": 41},
        {
            "id": 42,
            "workflow_id": 7,
            "event": "push",
            "head_branch": REF,
            "head_sha": SHA,
        },
    ],
)
def test_wait_for_workflow_rejects_non_authoritative_identity(
    monkeypatch: pytest.MonkeyPatch, run: dict[str, Any]
) -> None:
    """Reject a mismatched run ID or workflow identity.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing API and time helpers.
        run (dict[str, Any]): Invalid workflow run response.
    """
    monkeypatch.setattr(
        verify,
        "github_api",
        lambda _arguments: {"id": 7} if "/workflows/" in _arguments[0] else run,
    )
    monkeypatch.setattr(verify.time, "monotonic", lambda: 0.0)

    with pytest.raises(verify.GitHubCommandError):
        verify.wait_for_workflow(
            REPOSITORY, "validate.yml", REF, SHA, set(), deadline=1.0, expected_run_id=42
        )


def test_verify_jobs_rejects_incomplete_required_check_results(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Reject required checks unless every requested job has one successful result.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing the API helper.
    """
    monkeypatch.setattr(
        verify,
        "github_api",
        lambda _arguments: {
            "total_count": 4,
            "jobs": [
                {"name": "required", "conclusion": "success"},
                {"name": "duplicate", "conclusion": "success"},
                {"name": "duplicate", "conclusion": "success"},
                {"name": "failed", "conclusion": "failure"},
            ],
        },
    )

    with pytest.raises(verify.GitHubCommandError):
        verify.verify_jobs(REPOSITORY, 42, {"required", "duplicate", "failed", "missing"})


def test_github_api_rejects_malformed_or_non_authoritative_http_response(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Fail closed for absent CLI, malformed JSON, and wrong dispatch status.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing CLI discovery and execution.
    """
    monkeypatch.setattr(shutil, "which", lambda _name: None)
    with pytest.raises(verify.GitHubCommandError):
        verify.github_api([])

    monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/gh")
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *_args, **_kwargs: SimpleNamespace(
            returncode=0, stdout="HTTP/2 204 No Content\n\n", stderr=""
        ),
    )
    with pytest.raises(verify.GitHubCommandError):
        verify.github_api([], expected_status=200)

    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *_args, **_kwargs: SimpleNamespace(returncode=0, stdout="{", stderr=""),
    )
    with pytest.raises(json.JSONDecodeError):
        verify.github_api([])


def test_verify_check_suite_rejects_mismatched_candidate_commit(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Reject a check suite that is tied to a different candidate commit.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing the API helper.
    """
    monkeypatch.setattr(
        verify,
        "github_api",
        lambda _arguments: {"head_sha": "b" * 40, "app": {"slug": "github-actions"}},
    )

    with pytest.raises(verify.GitHubCommandError):
        verify.verify_check_suite(REPOSITORY, {"check_suite_id": 99}, SHA)


def test_publish_verified_status_attests_exact_check_and_run(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Publish a successful commit status only for a verified check and run.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing the API helper.
    """
    calls: list[list[str]] = []

    def fake_api(arguments: Sequence[str]) -> dict[str, Any]:
        """Record a status request and return GitHub's confirmation.

        Args:
            arguments (Sequence[str]): GitHub CLI API arguments.

        Returns:
            dict[str, Any]: Confirmed successful status context.
        """
        calls.append(list(arguments))
        return {"context": "required", "state": "success"}

    monkeypatch.setattr(verify, "github_api", fake_api)

    verify.publish_verified_status(REPOSITORY, SHA, "required", 42)

    arguments = calls[0]
    assert f"repos/{REPOSITORY}/statuses/{SHA}" in arguments
    assert "state=success" in arguments
    assert "context=required" in arguments
    assert f"target_url=https://github.com/{REPOSITORY}/actions/runs/42" in arguments


def test_main_publishes_no_status_until_every_workflow_is_verified(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Do not attest any check when a later release-gate workflow fails.

    Args:
        monkeypatch (pytest.MonkeyPatch): Fixture for replacing orchestration helpers.
    """
    published: list[tuple[str, str, str, int]] = []
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "verify_release_checks.py",
            "--repository",
            REPOSITORY,
            "--ref",
            REF,
            "--sha",
            SHA,
            "--required-check",
            "first.yml::first",
            "--required-check",
            "second.yml::second",
        ],
    )
    monkeypatch.setattr(
        verify,
        "dispatch_workflow",
        lambda _repository, workflow, _ref, _sha: 41 if workflow == "first.yml" else 42,
    )

    def fake_wait(
        _repository: str,
        workflow: str,
        _ref: str,
        _sha: str,
        _required_checks: set[str],
        _deadline: float,
        expected_run_id: int,
    ) -> int:
        """Return the first run ID and simulate a later gate failure.

        Args:
            _repository (str): Unused GitHub repository name.
            workflow (str): Workflow filename being verified.
            _ref (str): Unused validation branch name.
            _sha (str): Unused candidate commit SHA.
            _required_checks (set[str]): Unused required job names.
            _deadline (float): Unused verification deadline.
            expected_run_id (int): Authoritative dispatched workflow run ID.

        Returns:
            int: The authoritative run ID for the first workflow.

        Raises:
            verify.GitHubCommandError: When verifying the simulated second workflow.
        """
        if workflow == "second.yml":
            raise verify.GitHubCommandError("second gate failed")
        return expected_run_id

    monkeypatch.setattr(verify, "wait_for_workflow", fake_wait)
    monkeypatch.setattr(
        verify,
        "publish_verified_status",
        lambda repository, sha, check, run_id: published.append((repository, sha, check, run_id)),
    )

    assert verify.main() == 1
    assert published == []


def test_release_workflow_uses_published_event_and_guarded_package_promotion() -> None:
    """Require published releases, exact gates, and lease-guarded package promotion."""
    workflow = _workflow_text("release.yml")

    assert re.search(r"(?ms)^on:\s+release:\s+types:\s*\[published\]", workflow)
    assert re.search(r"(?m)^      statuses:\s*write\s*$", workflow)
    dispatch = _step_containing(workflow, "verify_release_checks.py")
    required_checks = set(re.findall(r"--required-check\s+['\"]([^'\"]+)", dispatch))
    assert required_checks == {
        "pytest_check.yml::pytest check and post coverage",
        "docs.yml::build-docs",
        "uv-lock-check.yml::Validate uv lock consistency",
        "prek-autofix-review.yml::review",
    }

    promotion = _normalized(_step_containing(workflow, "git push --atomic"))
    assert '--force-with-lease="refs/heads/$RELEASE_TARGET:$TARGET_SHA"' in promotion
    assert '--force-with-lease="refs/tags/$RELEASE_TAG:$ORIGINAL_TAG_OID"' in promotion

    metadata = _normalized(_step_containing(workflow, 'tag_sha="$(git rev-parse'))
    assert 'git show "$tag_sha:aiopnsense/const.py"' in metadata
    assert 'git merge-base --is-ancestor "$tag_sha" "$target_sha"' in metadata
    assert 'git checkout --detach "$source_sha"' in metadata

    final_verification = _normalized(_step_containing(workflow, 'expected_sha="$STABLE_SHA"'))
    assert '"$expected_sha" "refs/remotes/origin/$RELEASE_TARGET"' in final_verification
    assert "git diff --quiet -- aiopnsense/const.py" in final_verification

    upload = _normalized(_step_containing(workflow, "gh release upload"))
    assert 'gh release upload "$RELEASE_TAG" dist/* --clobber' in upload
    assert "pypa/gh-action-pypi-publish@release/v1" in workflow


@pytest.mark.parametrize(
    "workflow_name",
    ["pytest_check.yml", "docs.yml", "uv-lock-check.yml", "prek-autofix-review.yml"],
)
def test_release_gate_workflows_require_and_checkout_exact_sha(
    workflow_name: str,
) -> None:
    """Require every dispatched gate to validate and check out the candidate SHA.

    Args:
        workflow_name (str): Workflow filename under test.
    """
    workflow = _workflow_text(workflow_name)

    assert re.search(
        r"(?ms)^  workflow_dispatch:\s+inputs:\s+expected_sha:.*?"
        r"^        required:\s*true\s+^        type:\s*string\s*$",
        workflow,
    )
    guard = _normalized(_step_containing(workflow, "WORKFLOW_SHA:"))
    assert '[[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]' in guard
    assert 'test "$WORKFLOW_SHA" = "$EXPECTED_SHA"' in guard
    checkout = _normalized(_step_containing(workflow, "actions/checkout@"))
    assert re.search(r"ref:\s*\$\{\{[^}]*inputs\.expected_sha[^}]*}}", checkout)
    assert "persist-credentials: false" in checkout
