"""Step definitions for security scan hooks configuration feature."""

from __future__ import annotations

import ast
import re
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 pre-commit config file exists")
def step_precommit_config_exists(context: Any) -> None:
    config_path = PROJECT_ROOT / ".pre-commit-config.yaml"
    if not config_path.is_file():
        raise FileNotFoundError(f".pre-commit-config.yaml not found at {config_path}")
    context.precommit_config_path = config_path
    context.precommit_config_content = config_path.read_text(encoding="utf-8")


@when("I parse the pre-commit configuration")
def step_parse_precommit_config(context: Any) -> None:
    content = context.precommit_config_content
    data = yaml.safe_load(content)
    hooks: list[dict[str, Any]] = []
    for repo in data.get("repos", []):
        for hook in repo.get("hooks", []):
            hooks.append(hook)
    context.precommit_hooks = hooks
    context.precommit_data = data


@then('a hook with id "{hook_id}" should be declared')
def step_hook_declared(context: Any, hook_id: str) -> None:
    hooks = context.precommit_hooks
    found = any(h.get("id") == hook_id for h in hooks)
    if not found:
        ids = [h.get("id") for h in hooks]
        raise AssertionError(
            f"Hook '{hook_id}' not found in pre-commit config. Found: {ids}"
        )


@then('the "{hook_id}" hook should have a files pattern matching src')
def step_hook_files_src(context: Any, hook_id: str) -> None:
    hooks = context.precommit_hooks
    for hook in hooks:
        if hook.get("id") == hook_id:
            files_pattern = hook.get("files", "")
            if "src" not in files_pattern:
                raise AssertionError(
                    f"Hook '{hook_id}' files pattern '{files_pattern}' "
                    f"does not match src"
                )
            return
    raise AssertionError(f"Hook '{hook_id}' not found in pre-commit config")


@then('the "{hook_id}" hook args should reference pyproject.toml')
def step_hook_args_pyproject(context: Any, hook_id: str) -> None:
    hooks = context.precommit_hooks
    for hook in hooks:
        if hook.get("id") == hook_id:
            args = hook.get("args", [])
            args_str = " ".join(str(a) for a in args)
            if "pyproject.toml" not in args_str:
                raise AssertionError(
                    f"Hook '{hook_id}' args do not reference pyproject.toml: {args}"
                )
            return
    raise AssertionError(f"Hook '{hook_id}' not found in pre-commit config")


@then('the "{hook_id}" hook entry should reference .semgrep.yml')
def step_hook_entry_semgrep_yml(context: Any, hook_id: str) -> None:
    hooks = context.precommit_hooks
    for hook in hooks:
        if hook.get("id") == hook_id:
            entry = hook.get("entry", "")
            if ".semgrep.yml" not in entry:
                raise AssertionError(
                    f"Hook '{hook_id}' entry does not reference .semgrep.yml: {entry}"
                )
            return
    raise AssertionError(f"Hook '{hook_id}' not found in pre-commit config")


@when("I parse the dev dependencies from pyproject.toml")
def step_parse_dev_deps(context: Any) -> None:
    import tomllib

    content = context.pyproject_content
    data = tomllib.loads(content)
    dev_deps = data.get("project", {}).get("optional-dependencies", {}).get("dev", [])
    context.dev_dependencies = dev_deps


@then('"{package}" should be in the dev dependencies')
def step_package_in_dev_deps(context: Any, package: str) -> None:
    deps = context.dev_dependencies
    found = any(package in dep for dep in deps)
    if not found:
        raise AssertionError(f"'{package}' not found in dev dependencies: {deps}")


@when("I parse the nox session names from noxfile.py")
def step_parse_nox_sessions(context: Any) -> None:
    content = context.noxfile_content
    tree = ast.parse(content)
    sessions: list[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            for decorator in node.decorator_list:
                if isinstance(decorator, ast.Call):
                    func = decorator.func
                    if (isinstance(func, ast.Attribute) and func.attr == "session") or (
                        isinstance(func, ast.Name) and func.id == "session"
                    ):
                        sessions.append(node.name)
                elif isinstance(decorator, ast.Attribute):
                    if decorator.attr == "session":
                        sessions.append(node.name)
    context.nox_sessions = sessions


@then('"{session_name}" should be a registered nox session')
def step_session_registered(context: Any, session_name: str) -> None:
    sessions = context.nox_sessions
    if session_name not in sessions:
        raise AssertionError(
            f"Session '{session_name}' not found in nox sessions: {sessions}"
        )


@when("I read the security_scan session source from noxfile.py")
def step_read_security_session(context: Any) -> None:
    content = context.noxfile_content
    pattern = r"(def security_scan\(.*?\n(?:(?:    .*\n|[ \t]*\n)*))"
    match = re.search(pattern, content)
    if match is None:
        raise ValueError("security_scan function not found in noxfile.py")
    context.security_session_source = match.group(1)


@then("the session source should contain a bandit invocation")
def step_session_has_bandit(context: Any) -> None:
    source = context.security_session_source
    if "bandit" not in source.lower():
        raise AssertionError(
            "security_scan session does not contain a bandit invocation"
        )


@then("the session source should contain a semgrep invocation")
def step_session_has_semgrep(context: Any) -> None:
    source = context.security_session_source
    if "semgrep" not in source.lower():
        raise AssertionError(
            "security_scan session does not contain a semgrep invocation"
        )


@then("the session source should contain a vulture invocation")
def step_session_has_vulture(context: Any) -> None:
    source = context.security_session_source
    if "vulture" not in source.lower():
        raise AssertionError(
            "security_scan session does not contain a vulture invocation"
        )


@given("the semgrep config file exists")
def step_semgrep_config_exists(context: Any) -> None:
    semgrep_path = PROJECT_ROOT / ".semgrep.yml"
    if not semgrep_path.is_file():
        raise FileNotFoundError(f".semgrep.yml not found at {semgrep_path}")
    context.semgrep_config_path = semgrep_path
    context.semgrep_config_content = semgrep_path.read_text(encoding="utf-8")


@when("I parse the semgrep configuration")
def step_parse_semgrep_config(context: Any) -> None:
    content = context.semgrep_config_content
    data = yaml.safe_load(content)
    context.semgrep_config = data


@then("the semgrep config should contain at least {count:d} rules")
def step_semgrep_rule_count(context: Any, count: int) -> None:
    config = context.semgrep_config
    rules = config.get("rules", [])
    actual_count = len(rules)
    if actual_count < count:
        raise AssertionError(
            f"Expected at least {count} semgrep rules, found {actual_count}"
        )
