"""Step definitions for coverage threshold enforcement feature."""

from __future__ import annotations

import ast
import re
import tomllib
from pathlib import Path
from typing import Any

import yaml
from behave import given, then, when

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent


@given("the noxfile.py exists")
def step_noxfile_exists(context: Any) -> None:
    noxfile = PROJECT_ROOT / "noxfile.py"
    if not noxfile.is_file():
        raise FileNotFoundError(f"noxfile.py not found at {noxfile}")
    context.noxfile_path = noxfile
    context.noxfile_content = noxfile.read_text(encoding="utf-8")


@given("the pyproject.toml exists")
def step_pyproject_exists(context: Any) -> None:
    pyproject = PROJECT_ROOT / "pyproject.toml"
    if not pyproject.is_file():
        raise FileNotFoundError(f"pyproject.toml not found at {pyproject}")
    context.pyproject_path = pyproject
    context.pyproject_content = pyproject.read_text(encoding="utf-8")


@given("the CI workflow file exists")
def step_ci_workflow_exists(context: Any) -> None:
    ci_file = PROJECT_ROOT / ".forgejo" / "workflows" / "ci.yml"
    if not ci_file.is_file():
        raise FileNotFoundError(f"CI workflow not found at {ci_file}")
    context.ci_file_path = ci_file
    context.ci_file_content = ci_file.read_text(encoding="utf-8")


@when("I parse the COVERAGE_THRESHOLD constant from noxfile.py")
def step_parse_threshold(context: Any) -> None:
    content = context.noxfile_content
    tree = ast.parse(content)
    threshold = None
    for node in ast.walk(tree):
        if isinstance(node, ast.Assign):
            for target in node.targets:
                if (
                    isinstance(target, ast.Name)
                    and target.id == "COVERAGE_THRESHOLD"
                    and isinstance(node.value, ast.Constant)
                ):
                    threshold = node.value.value
    if threshold is None:
        raise ValueError("COVERAGE_THRESHOLD constant not found in noxfile.py")
    context.coverage_threshold = threshold


@then("the coverage threshold should be {expected:d}")
def step_check_threshold(context: Any, expected: int) -> None:
    actual = context.coverage_threshold
    if actual != expected:
        raise AssertionError(f"Expected coverage threshold {expected}, got {actual}")


@when("I read the coverage_report session source from noxfile.py")
def step_read_coverage_session(context: Any) -> None:
    content = context.noxfile_content
    # Extract the coverage_report function body using regex
    # Find the function definition and capture everything until the next
    # top-level definition or end of file
    pattern = r"(def coverage_report\(.*?\n(?:(?:    .*\n|[ \t]*\n)*))"
    match = re.search(pattern, content)
    if match is None:
        raise ValueError("coverage_report function not found in noxfile.py")
    context.coverage_session_source = match.group(1)


@then("the session should contain a fail-under argument matching the threshold")
def step_check_fail_under(context: Any) -> None:
    source = context.coverage_session_source
    # The session should reference COVERAGE_THRESHOLD in a fail-under argument
    if "fail-under" not in source:
        raise AssertionError(
            "coverage_report session does not contain --fail-under argument"
        )
    if "COVERAGE_THRESHOLD" not in source:
        raise AssertionError(
            "coverage_report session does not reference COVERAGE_THRESHOLD constant"
        )


@when("I parse the coverage run configuration from pyproject.toml")
def step_parse_coverage_run(context: Any) -> None:
    content = context.pyproject_content
    data = tomllib.loads(content)
    coverage_run = data.get("tool", {}).get("coverage", {}).get("run", {})
    context.coverage_run_config = coverage_run


@then("branch coverage should be enabled")
def step_check_branch_coverage(context: Any) -> None:
    config = context.coverage_run_config
    if not config.get("branch", False):
        raise AssertionError("Branch coverage is not enabled in coverage.run config")


@then('the source list should include "{source}"')
def step_check_source_list(context: Any, source: str) -> None:
    config = context.coverage_run_config
    sources = config.get("source", [])
    if source not in sources:
        raise AssertionError(f"'{source}' not in coverage.run source list: {sources}")


@then("the data file should be under the build directory")
def step_check_data_file(context: Any) -> None:
    config = context.coverage_run_config
    data_file = config.get("data_file", "")
    if not data_file.startswith("build/"):
        raise AssertionError(f"Coverage data file '{data_file}' is not under build/")


@when("I parse the coverage html configuration from pyproject.toml")
def step_parse_coverage_html(context: Any) -> None:
    content = context.pyproject_content
    data = tomllib.loads(content)
    coverage_html = data.get("tool", {}).get("coverage", {}).get("html", {})
    context.coverage_html_config = coverage_html


@then("the html directory should be under the build directory")
def step_check_html_dir(context: Any) -> None:
    config = context.coverage_html_config
    directory = config.get("directory", "")
    if not directory.startswith("build/"):
        raise AssertionError(
            f"Coverage HTML directory '{directory}' is not under build/"
        )


@when("I parse the coverage xml configuration from pyproject.toml")
def step_parse_coverage_xml(context: Any) -> None:
    content = context.pyproject_content
    data = tomllib.loads(content)
    coverage_xml = data.get("tool", {}).get("coverage", {}).get("xml", {})
    context.coverage_xml_config = coverage_xml


@then("the xml output should be under the build directory")
def step_check_xml_output(context: Any) -> None:
    config = context.coverage_xml_config
    output = config.get("output", "")
    if not output.startswith("build/"):
        raise AssertionError(f"Coverage XML output '{output}' is not under build/")


@when("I parse the coverage job from the CI workflow")
def step_parse_ci_coverage(context: Any) -> None:
    content = context.ci_file_content
    data = yaml.safe_load(content)
    jobs = data.get("jobs", {})
    coverage_job = jobs.get("coverage", {})
    if not coverage_job:
        raise ValueError("No 'coverage' job found in CI workflow")
    context.ci_coverage_job = coverage_job


@then("the coverage job should run nox -s coverage_report")
def step_check_ci_coverage_nox(context: Any) -> None:
    job = context.ci_coverage_job
    steps = job.get("steps", [])
    found = False
    for step in steps:
        run_cmd = step.get("run", "")
        if "nox -s coverage_report" in run_cmd:
            found = True
            break
    if not found:
        raise AssertionError(
            "CI coverage job does not contain 'nox -s coverage_report'"
        )


@then("the coverage job should depend on lint and typecheck")
def step_check_ci_coverage_deps(context: Any) -> None:
    job = context.ci_coverage_job
    needs = job.get("needs", [])
    if "lint" not in needs:
        raise AssertionError(
            f"CI coverage job does not depend on 'lint': needs={needs}"
        )
    if "typecheck" not in needs:
        raise AssertionError(
            f"CI coverage job does not depend on 'typecheck': needs={needs}"
        )


@then("the session should emit a COVERAGE OK summary line")
def step_check_ok_summary(context: Any) -> None:
    source = context.coverage_session_source
    if "COVERAGE OK:" not in source:
        raise AssertionError(
            "coverage_report session does not emit 'COVERAGE OK:' summary"
        )


@then("the session should emit a COVERAGE FAILED summary line")
def step_check_failed_summary(context: Any) -> None:
    source = context.coverage_session_source
    if "COVERAGE FAILED:" not in source:
        raise AssertionError(
            "coverage_report session does not emit 'COVERAGE FAILED:' summary"
        )
