"""Step definitions for the Kubernetes Helm Chart feature."""

from __future__ import annotations

import re
import shutil
import subprocess
from pathlib import Path
from typing import Any

import yaml
from behave import given, then, when
from behave.runner import Context


def _project_root() -> Path:
    """Return the project root directory.

    Walks up from this file until we find pyproject.toml.
    """
    current: Path = Path(__file__).resolve()
    for parent in current.parents:
        if (parent / "pyproject.toml").exists():
            return parent
    raise RuntimeError("Could not find project root (no pyproject.toml found)")


def _skip_if_helm_missing(context: Context, *, reason: str) -> bool:
    """Skip the current scenario when Helm CLI is unavailable.

    Returns ``True`` when Helm is missing (scenario was skipped).
    """
    if shutil.which("helm") is None:
        context.scenario.skip(reason)
        return True
    return False


def _render_chart(context: Context, *, set_overrides: list[str] | None = None) -> None:
    """Run ``helm template`` and store the subprocess result on context."""
    if _skip_if_helm_missing(
        context,
        reason="Helm CLI is required for rendered Helm behavior scenarios",
    ):
        context.helm_render_result = None
        return

    command: list[str] = [
        "helm",
        "template",
        "cleveragents",
        str(context.project_root / "k8s"),
        "--dependency-update",
    ]
    for item in set_overrides or []:
        command.extend(["--set", item])

    context.helm_render_result = subprocess.run(
        command,
        cwd=str(context.project_root),
        capture_output=True,
        text=True,
        check=False,
    )


@given("the project root directory")
def step_given_project_root(context: Context) -> None:
    context.project_root = _project_root()


@when("I read the Helm chart metadata")
def step_read_chart_metadata(context: Context) -> None:
    chart_path: Path = context.project_root / "k8s" / "Chart.yaml"
    assert chart_path.exists(), f"Chart.yaml not found at {chart_path}"
    with open(chart_path, encoding="utf-8") as f:
        context.chart_metadata = yaml.safe_load(f)


@then('the chart API version should be "{version}"')
def step_chart_api_version(context: Context, version: str) -> None:
    assert context.chart_metadata.get("apiVersion") == version


@then('the chart name should be "{name}"')
def step_chart_name(context: Context, name: str) -> None:
    assert context.chart_metadata.get("name") == name


@then('the chart type should be "{chart_type}"')
def step_chart_type(context: Context, chart_type: str) -> None:
    assert context.chart_metadata.get("type") == chart_type


@then("the chart version should be set")
def step_chart_version_set(context: Context) -> None:
    assert "version" in context.chart_metadata
    assert context.chart_metadata.get("version")


@then('the chart should have a dependency named "{dep_name}"')
def step_chart_has_dependency(context: Context, dep_name: str) -> None:
    deps: list[dict[str, str]] = context.chart_metadata.get("dependencies", [])
    dep_names: list[str] = [d.get("name", "") for d in deps]
    assert dep_name in dep_names, f"Dependency '{dep_name}' not found in {dep_names}"
    for dep in deps:
        if dep.get("name") == dep_name:
            context.matched_dependency = dep
            break


@then('the Redis dependency should be conditional on "{condition}"')
def step_redis_dependency_condition(context: Context, condition: str) -> None:
    dep: dict[str, str] = context.matched_dependency
    assert dep.get("condition") == condition, (
        f"Expected condition '{condition}', got '{dep.get('condition')}'"
    )


@when("I read the Helm chart values")
def step_read_chart_values(context: Context) -> None:
    values_path: Path = context.project_root / "k8s" / "values.yaml"
    assert values_path.exists(), f"values.yaml not found at {values_path}"
    with open(values_path, encoding="utf-8") as f:
        context.chart_values = yaml.safe_load(f)


@then('the values should contain key "{key}"')
def step_values_contains_key(context: Context, key: str) -> None:
    assert key in context.chart_values, (
        f"Key '{key}' not found in values.yaml. "
        f"Available keys: {list(context.chart_values.keys())}"
    )


@then("the default replica count should be {count:d}")
def step_default_replica_count(context: Context, count: int) -> None:
    assert context.chart_values.get("replicaCount") == count


@then("the resources should have CPU and memory limits")
def step_resources_have_limits(context: Context) -> None:
    resources: dict[str, Any] = context.chart_values.get("resources", {})
    assert "limits" in resources, "Missing 'limits' in resources"
    limits: dict[str, str] = resources.get("limits", {})
    assert "cpu" in limits, "Missing 'cpu' in resource limits"
    assert "memory" in limits, "Missing 'memory' in resource limits"


@then("the resources should have CPU and memory requests")
def step_resources_have_requests(context: Context) -> None:
    resources: dict[str, Any] = context.chart_values.get("resources", {})
    assert "requests" in resources, "Missing 'requests' in resources"
    reqs: dict[str, str] = resources.get("requests", {})
    assert "cpu" in reqs, "Missing 'cpu' in resource requests"
    assert "memory" in reqs, "Missing 'memory' in resource requests"


@then("the ingress should have an enabled toggle")
def step_ingress_has_enabled(context: Context) -> None:
    ingress: dict[str, Any] = context.chart_values.get("ingress", {})
    assert "enabled" in ingress, "Missing 'enabled' toggle in ingress"
    assert isinstance(ingress.get("enabled"), bool), "'enabled' should be a boolean"


@then("the ingress should have a TLS configuration section")
def step_ingress_has_tls(context: Context) -> None:
    ingress: dict[str, Any] = context.chart_values.get("ingress", {})
    assert "tls" in ingress, "Missing 'tls' section in ingress"


@then("the Redis configuration should have an enabled toggle")
def step_redis_has_enabled(context: Context) -> None:
    redis: dict[str, Any] = context.chart_values.get("redis", {})
    assert "enabled" in redis, "Missing 'enabled' toggle in redis"
    assert isinstance(redis.get("enabled"), bool), "'enabled' should be a boolean"


@then("the Redis configuration should have authentication settings")
def step_redis_has_auth(context: Context) -> None:
    redis: dict[str, Any] = context.chart_values.get("redis", {})
    assert "auth" in redis, "Missing 'auth' section in redis"
    auth: dict[str, Any] = redis.get("auth", {})
    assert "enabled" in auth, "Missing 'enabled' in redis auth"


@then("the server config should have host setting")
def step_server_has_host(context: Context) -> None:
    server: dict[str, Any] = context.chart_values.get("server", {})
    assert "host" in server, "Missing 'host' in server config"


@then("the server config should have port setting")
def step_server_has_port(context: Context) -> None:
    server: dict[str, Any] = context.chart_values.get("server", {})
    assert "port" in server, "Missing 'port' in server config"


@then("the server config should have workers setting")
def step_server_has_workers(context: Context) -> None:
    server: dict[str, Any] = context.chart_values.get("server", {})
    assert "workers" in server, "Missing 'workers' in server config"


@then("the database config should support existing secrets")
def step_database_supports_secrets(context: Context) -> None:
    database: dict[str, Any] = context.chart_values.get("database", {})
    assert "existingSecret" in database, "Missing 'existingSecret' in database config"
    assert "existingSecretKey" in database, (
        "Missing 'existingSecretKey' in database config"
    )


@then("the pod security context should enforce non-root execution")
def step_pod_security_nonroot(context: Context) -> None:
    pod_sc: dict[str, Any] = context.chart_values.get("podSecurityContext", {})
    assert pod_sc.get("runAsNonRoot") is True, (
        "podSecurityContext.runAsNonRoot should be true"
    )
    assert pod_sc.get("runAsUser") == 1000, (
        "podSecurityContext.runAsUser should be 1000"
    )


@then("the container security context should drop all capabilities")
def step_container_security_caps(context: Context) -> None:
    sc: dict[str, Any] = context.chart_values.get("securityContext", {})
    caps: dict[str, list[str]] = sc.get("capabilities", {})
    assert "ALL" in caps.get("drop", []), "securityContext should drop ALL capabilities"


@then("the container security context should have a seccomp profile")
def step_container_security_seccomp(context: Context) -> None:
    sc: dict[str, Any] = context.chart_values.get("securityContext", {})
    seccomp: dict[str, str] = sc.get("seccompProfile", {})
    assert seccomp.get("type") == "RuntimeDefault", (
        f"Expected seccompProfile type 'RuntimeDefault', got '{seccomp.get('type')}'"
    )


@then("the container security context should set readOnlyRootFilesystem to true")
def step_container_security_readonly_root(context: Context) -> None:
    sc: dict[str, Any] = context.chart_values.get("securityContext", {})
    assert sc.get("readOnlyRootFilesystem") is True, (
        "securityContext.readOnlyRootFilesystem should be true"
    )


@then("the container security context should set allowPrivilegeEscalation to false")
def step_container_security_no_privilege_escalation(context: Context) -> None:
    sc: dict[str, Any] = context.chart_values.get("securityContext", {})
    assert sc.get("allowPrivilegeEscalation") is False, (
        "securityContext.allowPrivilegeEscalation should be false"
    )


@then('the chart file "{filepath}" should exist')
def step_chart_file_should_exist(context: Context, filepath: str) -> None:
    full_path: Path = context.project_root / filepath
    assert full_path.exists(), f"File not found: {full_path}"


@when('I read the template "{filepath}"')
def step_read_template(context: Context, filepath: str) -> None:
    template_path: Path = context.project_root / filepath
    assert template_path.exists(), f"Template not found at {template_path}"
    context.template_content = template_path.read_text(encoding="utf-8")


@then('the template should contain "{text}"')
def step_template_should_contain(context: Context, text: str) -> None:
    pattern: re.Pattern[str] = re.compile(re.escape(text), re.MULTILINE)
    assert pattern.search(context.template_content), (
        f"Template does not contain '{text}'"
    )


@then('the template should match pattern "{pattern}"')
def step_template_should_match_pattern(context: Context, pattern: str) -> None:
    regex: re.Pattern[str] = re.compile(pattern, re.MULTILINE)
    assert regex.search(context.template_content), (
        f"Template does not match pattern '{pattern}'"
    )


@when("I read the server Dockerfile")
def step_read_server_dockerfile(context: Context) -> None:
    dockerfile_path: Path = context.project_root / "Dockerfile.server"
    assert dockerfile_path.exists(), f"Dockerfile.server not found at {dockerfile_path}"
    context.dockerfile_content = dockerfile_path.read_text(encoding="utf-8")


@then("it should have a builder stage")
def step_dockerfile_has_builder_stage(context: Context) -> None:
    pattern: re.Pattern[str] = re.compile(r"^FROM\s+.*\s+AS\s+builder", re.MULTILINE)
    assert pattern.search(context.dockerfile_content), (
        "Dockerfile.server missing builder stage (expected FROM ... AS builder)"
    )


@then("it should have a runtime stage")
def step_dockerfile_has_runtime_stage(context: Context) -> None:
    lines: list[str] = context.dockerfile_content.splitlines()
    from_count: int = sum(1 for line in lines if re.match(r"^FROM\s+", line))
    assert from_count >= 2, (
        f"Expected at least 2 FROM statements for multi-stage build, found {from_count}"
    )


@then("it should create a non-root user with uid 1000")
def step_dockerfile_nonroot_user(context: Context) -> None:
    pattern: re.Pattern[str] = re.compile(r"useradd.*-u\s+1000")
    assert pattern.search(context.dockerfile_content), (
        "Dockerfile.server should create a user with uid 1000 via useradd"
    )
    assert "USER appuser" in context.dockerfile_content, (
        "Dockerfile.server should switch to appuser"
    )


@then("it should expose port 8000")
def step_dockerfile_expose_port(context: Context) -> None:
    pattern: re.Pattern[str] = re.compile(r"^EXPOSE\s+8000", re.MULTILINE)
    assert pattern.search(context.dockerfile_content), (
        "Dockerfile.server should expose port 8000"
    )


@then("it should not use unpinned uv latest tag")
def step_dockerfile_pinned_uv(context: Context) -> None:
    assert "uv:latest" not in context.dockerfile_content, (
        "Dockerfile.server should pin the uv image version, not use :latest"
    )


@then("the uv image reference should use a specific semver tag")
def step_dockerfile_uv_semver(context: Context) -> None:
    pattern: re.Pattern[str] = re.compile(r"uv:\d+\.\d+\.\d+", re.MULTILINE)
    assert pattern.search(context.dockerfile_content), (
        "Dockerfile.server should pin uv to a specific semver version "
        "(e.g., uv:0.8.0), not a floating tag"
    )


@then("the Dockerfile should remove uv after installation")
def step_dockerfile_removes_uv(context: Context) -> None:
    pattern: re.Pattern[str] = re.compile(
        r"rm\s+.*(/usr/local/bin/uv|uv)", re.MULTILINE
    )
    assert pattern.search(context.dockerfile_content), (
        "Dockerfile.server should remove uv from runtime image after "
        "package installation to reduce attack surface"
    )


@then('the ENTRYPOINT should use "{entrypoint}"')
def step_dockerfile_entrypoint(context: Context, entrypoint: str) -> None:
    """Assert that the Dockerfile ENTRYPOINT contains the expected command."""
    pattern: re.Pattern[str] = re.compile(r"^ENTRYPOINT\s+\[.*\]", re.MULTILINE)
    match = pattern.search(context.dockerfile_content)
    assert match is not None, "Dockerfile.server is missing an ENTRYPOINT instruction"
    entrypoint_line = match.group(0)
    # Verify each token of the expected entrypoint appears in the ENTRYPOINT line
    for token in entrypoint.split():
        assert token in entrypoint_line, (
            f"Expected ENTRYPOINT to contain '{token}', but got: {entrypoint_line!r}"
        )


@then('the CMD should include "{cmd_a}" and "{cmd_b}"')
def step_dockerfile_cmd_includes(context: Context, cmd_a: str, cmd_b: str) -> None:
    """Assert that the Dockerfile CMD contains the expected subcommand tokens."""
    pattern: re.Pattern[str] = re.compile(r"^CMD\s+\[.*\]", re.MULTILINE)
    match = pattern.search(context.dockerfile_content)
    assert match is not None, "Dockerfile.server is missing a CMD instruction"
    cmd_line = match.group(0)
    assert cmd_a in cmd_line, (
        f"Expected CMD to contain '{cmd_a}', but got: {cmd_line!r}"
    )
    assert cmd_b in cmd_line, (
        f"Expected CMD to contain '{cmd_b}', but got: {cmd_line!r}"
    )


@when("I read the deployment README")
def step_read_deployment_readme(context: Context) -> None:
    readme_path: Path = context.project_root / "k8s" / "README.md"
    assert readme_path.exists(), f"k8s/README.md not found at {readme_path}"
    context.readme_content = readme_path.read_text(encoding="utf-8")


@then("it should contain quick start instructions")
def step_readme_has_quickstart(context: Context) -> None:
    assert "Quick Start" in context.readme_content, (
        "README should contain 'Quick Start' section"
    )


@then("it should contain TLS configuration documentation")
def step_readme_has_tls_docs(context: Context) -> None:
    assert "TLS" in context.readme_content, "README should document TLS configuration"


@then("it should contain Redis configuration documentation")
def step_readme_has_redis_docs(context: Context) -> None:
    assert "Redis" in context.readme_content, (
        "README should document Redis configuration"
    )


@then("it should contain scaling instructions")
def step_readme_has_scaling_docs(context: Context) -> None:
    assert (
        "Scaling" in context.readme_content or "replicaCount" in context.readme_content
    ), "README should document scaling instructions"


# ---------------------------------------------------------------------------
# Probes validation
# ---------------------------------------------------------------------------


@then("the liveness probe should target the live endpoint")
def step_liveness_probe_live(context: Context) -> None:
    probe: dict[str, Any] = context.chart_values.get("livenessProbe", {})
    http_get: dict[str, Any] = probe.get("httpGet", {})
    assert http_get.get("path") == "/live", (
        f"Expected liveness probe path /live, got {http_get.get('path')}"
    )


@then("the readiness probe should target the ready endpoint")
def step_readiness_probe_ready(context: Context) -> None:
    probe: dict[str, Any] = context.chart_values.get("readinessProbe", {})
    http_get: dict[str, Any] = probe.get("httpGet", {})
    assert http_get.get("path") == "/ready", (
        f"Expected readiness probe path /ready, got {http_get.get('path')}"
    )


# ---------------------------------------------------------------------------
# Image defaults validation
# ---------------------------------------------------------------------------


@then('the image pull policy should be "{policy}"')
def step_image_pull_policy(context: Context, policy: str) -> None:
    image: dict[str, Any] = context.chart_values.get("image", {})
    assert image.get("pullPolicy") == policy, (
        f"Expected pullPolicy '{policy}', got '{image.get('pullPolicy')}'"
    )


@then("the image tag should default to empty for appVersion fallback")
def step_image_tag_default_empty(context: Context) -> None:
    image: dict[str, Any] = context.chart_values.get("image", {})
    assert image.get("tag") == "", (
        f"Expected empty tag for appVersion fallback, got '{image.get('tag')}'"
    )


# ---------------------------------------------------------------------------
# Service defaults validation
# ---------------------------------------------------------------------------


@then('the service type should be "{svc_type}"')
def step_service_type(context: Context, svc_type: str) -> None:
    service: dict[str, Any] = context.chart_values.get("service", {})
    assert service.get("type") == svc_type, (
        f"Expected service type '{svc_type}', got '{service.get('type')}'"
    )


@then("the service port should be {port:d}")
def step_service_port(context: Context, port: int) -> None:
    service: dict[str, Any] = context.chart_values.get("service", {})
    assert service.get("port") == port, (
        f"Expected service port {port}, got {service.get('port')}"
    )


@when("I render the Helm chart without database configuration")
def step_render_without_database_configuration(context: Context) -> None:
    _render_chart(context)


@then("the render should fail with required database configuration error")
def step_render_fails_database_required(context: Context) -> None:
    result = context.helm_render_result
    if result is None:
        return
    assert result.returncode != 0, "Expected helm template to fail without DB config"
    combined_output = f"{result.stdout}\n{result.stderr}"
    assert "database configuration is required" in combined_output, (
        "Expected database required fail-fast error in Helm output"
    )


@when("I render the Helm chart with ingress enabled and no TLS")
def step_render_with_ingress_enabled_no_tls(context: Context) -> None:
    _render_chart(
        context,
        set_overrides=[
            "database.url=postgresql+asyncpg://user:pass@db:5432/cleveragents",
            "ingress.enabled=true",
            "ingress.allowInsecure=false",
        ],
    )


@then("the render should fail with required ingress TLS configuration error")
def step_render_fails_ingress_tls_required(context: Context) -> None:
    result = context.helm_render_result
    if result is None:
        return
    assert result.returncode != 0, (
        "Expected helm template to fail when ingress is enabled without TLS"
    )
    combined_output = f"{result.stdout}\n{result.stderr}"
    assert "ingress.tls is required" in combined_output, (
        "Expected ingress TLS required fail-fast error in Helm output"
    )


@when("I render the Helm chart with ingress enabled and insecure opt-out enabled")
def step_render_with_ingress_insecure_opt_out(context: Context) -> None:
    _render_chart(
        context,
        set_overrides=[
            "database.url=postgresql+asyncpg://user:pass@db:5432/cleveragents",
            "ingress.enabled=true",
            "ingress.allowInsecure=true",
        ],
    )


@then("the ingress render should succeed in insecure dev mode")
def step_render_succeeds_insecure_ingress_opt_out(context: Context) -> None:
    result = context.helm_render_result
    if result is None:
        return
    assert result.returncode == 0, (
        "Expected helm template to succeed when ingress.allowInsecure=true"
    )
    combined_output = f"{result.stdout}\n{result.stderr}"
    assert "kind: Ingress" in combined_output, (
        "Expected rendered output to include an Ingress resource"
    )


@when("I render the Helm chart with ingress enabled and TLS configured")
def step_render_with_ingress_tls_configured(context: Context) -> None:
    _render_chart(
        context,
        set_overrides=[
            "database.url=postgresql+asyncpg://user:pass@db:5432/cleveragents",
            "ingress.enabled=true",
            "ingress.allowInsecure=false",
            "ingress.tls[0].secretName=cleveragents-tls",
            "ingress.tls[0].hosts[0]=cleveragents.example.com",
        ],
    )


@then("the ingress render should succeed with TLS enabled")
def step_render_succeeds_with_ingress_tls(context: Context) -> None:
    result = context.helm_render_result
    if result is None:
        return
    assert result.returncode == 0, (
        "Expected helm template to succeed when ingress TLS is configured"
    )
    combined_output = f"{result.stdout}\n{result.stderr}"
    assert "kind: Ingress" in combined_output, (
        "Expected rendered output to include an Ingress resource"
    )
    assert "secretName: cleveragents-tls" in combined_output, (
        "Expected rendered Ingress to include configured TLS secret"
    )
