"""Step definitions for config_get_spec_output.feature.

Tests that `agents config get` output matches the specification:
- Rich output: three panels (Config, Origin, Resolution Chain)
- JSON output: standard envelope with all required fields
- Spec-required type strings and human-readable source names
"""

from __future__ import annotations

import json
import os
import re
import shutil
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import patch

from behave import given, then, use_step_matcher, when
from behave.runner import Context
from typer.testing import CliRunner

from cleveragents.cli.commands import config as config_mod
from cleveragents.cli.commands.config import app as config_app

_runner = CliRunner()

# Human-readable source names per spec
_SPEC_SOURCE_NAMES = {
    "CLI flag",
    "Env var",
    "Local file",
    "Project file",
    "Config file",
    "Default",
}


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _setup_spec_temp(context: Context) -> None:
    """Redirect config dir/path to a fresh temp directory."""
    if not hasattr(context, "_spec_tmpdir"):
        context._spec_tmpdir = tempfile.mkdtemp(prefix="cfg_spec_")
    tmp = Path(context._spec_tmpdir)
    context._spec_dir_patch = patch.object(config_mod, "_CONFIG_DIR", tmp)
    context._spec_path_patch = patch.object(
        config_mod, "_CONFIG_PATH", tmp / "config.toml"
    )
    context._spec_dir_patch.start()
    context._spec_path_patch.start()

    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers: list[Any] = []
    context._cleanup_handlers.append(lambda: _teardown_spec_temp(context))


def _teardown_spec_temp(context: Context) -> None:
    for attr in ("_spec_dir_patch", "_spec_path_patch"):
        patcher = getattr(context, attr, None)
        if patcher is not None and getattr(patcher, "_active", True):
            patcher.stop()
    tmpdir = getattr(context, "_spec_tmpdir", None)
    if tmpdir and os.path.isdir(tmpdir):
        shutil.rmtree(tmpdir, ignore_errors=True)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a clean config get spec test environment")
def step_spec_clean_env(context: Context) -> None:
    _setup_spec_temp(context)


# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------


use_step_matcher("re")


@when(r'I run spec config get "(?P<key>[^"]+)"')
def step_spec_run_get(context: Context, key: str) -> None:
    context.spec_result = _runner.invoke(config_app, ["get", key])


@when(r'I run spec config get "(?P<key>[^"]+)" with format "(?P<fmt>[^"]+)"')
def step_spec_run_get_fmt(context: Context, key: str, fmt: str) -> None:
    context.spec_result = _runner.invoke(config_app, ["get", key, "--format", fmt])


use_step_matcher("parse")


@given('I have set spec config "{key}" to "{value}"')
def step_spec_preset_config(context: Context, key: str, value: str) -> None:
    result = _runner.invoke(config_app, ["set", key, value])
    assert result.exit_code == 0, f"Pre-set failed: {result.output}"


# ---------------------------------------------------------------------------
# Then — basic
# ---------------------------------------------------------------------------


@then("the spec config get should succeed")
def step_spec_get_succeed(context: Context) -> None:
    assert context.spec_result.exit_code == 0, (
        f"Expected exit 0, got {context.spec_result.exit_code}: "
        f"{context.spec_result.output}"
    )


@then('the spec config get rich output should contain panel "{panel_title}"')
def step_spec_rich_contains_panel(context: Context, panel_title: str) -> None:
    output = context.spec_result.output
    assert panel_title in output, (
        f"Expected panel '{panel_title}' in Rich output: {output}"
    )


@then('the spec config get rich output should contain "{text}"')
def step_spec_rich_contains(context: Context, text: str) -> None:
    output = context.spec_result.output
    assert text in output, f"Expected '{text}' in Rich output: {output}"


@then('the spec config get rich output should not contain "{text}"')
def step_spec_rich_not_contains(context: Context, text: str) -> None:
    output = context.spec_result.output
    # Check that the text doesn't appear as a standalone type name
    # (e.g. "str" should not appear but "string" is fine)
    # We check for exact word boundary to avoid false positives
    # Look for the text as a standalone word (not part of "string", "integer", etc.)
    pattern = rf"\b{re.escape(text)}\b"
    # Exclude lines that contain "string", "integer", "boolean" etc.
    lines = output.split("\n")
    for line in lines:
        if re.search(pattern, line):
            # Check if this is a false positive (e.g. "str" inside "string")
            # Only flag if the match is not part of a longer type word
            if text == "str" and "string" in line:
                continue
            if text == "bool" and "boolean" in line:
                continue
            if text == "int" and "integer" in line:
                continue
            raise AssertionError(
                f"Found '{text}' as standalone word in Rich output line: {line!r}"
            )


# ---------------------------------------------------------------------------
# Then — JSON envelope
# ---------------------------------------------------------------------------


def _get_spec_json(context: Context) -> dict[str, Any]:
    """Parse the JSON output of the most recent ``spec config get`` invocation.

    The result is NOT cached on ``context`` because behave layers context
    attributes — a value set at a previous scenario's layer would shadow
    the current scenario's ``spec_result``, leaking stale JSON across
    scenarios and producing assertion failures that look semantic but
    are really staleness.
    """
    return json.loads(context.spec_result.output.strip())


@then(
    'the spec config get JSON envelope should have field "{field}" equal to "{value}"'
)
def step_spec_json_envelope_field_str(context: Context, field: str, value: str) -> None:
    envelope = _get_spec_json(context)
    assert field in envelope, (
        f"Field '{field}' not in envelope: {list(envelope.keys())}"
    )
    assert str(envelope[field]) == value, (
        f"Expected envelope['{field}'] == '{value}', got {envelope[field]!r}"
    )


@then(
    'the spec config get JSON envelope should have field "{field}" equal to {value:d}'
)
def step_spec_json_envelope_field_int(context: Context, field: str, value: int) -> None:
    envelope = _get_spec_json(context)
    assert field in envelope, (
        f"Field '{field}' not in envelope: {list(envelope.keys())}"
    )
    assert envelope[field] == value, (
        f"Expected envelope['{field}'] == {value}, got {envelope[field]!r}"
    )


@then('the spec config get JSON envelope should have field "{field}"')
def step_spec_json_envelope_has_field(context: Context, field: str) -> None:
    envelope = _get_spec_json(context)
    assert field in envelope, (
        f"Field '{field}' not in envelope: {list(envelope.keys())}"
    )


# ---------------------------------------------------------------------------
# Then — JSON data
# ---------------------------------------------------------------------------


def _get_spec_json_data(context: Context) -> dict[str, Any]:
    """Return the data object from the JSON envelope."""
    envelope = _get_spec_json(context)
    return envelope["data"]


@then('the spec config get JSON data should have field "{field}"')
def step_spec_json_data_has_field(context: Context, field: str) -> None:
    data = _get_spec_json_data(context)
    assert field in data, f"Field '{field}' not in JSON data: {list(data.keys())}"


@then('the spec config get JSON data field "{field}" should equal "{value}"')
def step_spec_json_data_field_str(context: Context, field: str, value: str) -> None:
    data = _get_spec_json_data(context)
    assert field in data, f"Field '{field}' not in JSON data: {list(data.keys())}"
    assert data[field] == value, (
        f"Expected data['{field}'] == '{value}', got {data[field]!r}"
    )


@then('the spec config get JSON data field "{field}" should be false')
def step_spec_json_data_field_false(context: Context, field: str) -> None:
    data = _get_spec_json_data(context)
    assert field in data, f"Field '{field}' not in JSON data: {list(data.keys())}"
    assert data[field] is False, (
        f"Expected data['{field}'] == False, got {data[field]!r}"
    )


@then('the spec config get JSON data field "{field}" should be true')
def step_spec_json_data_field_true(context: Context, field: str) -> None:
    data = _get_spec_json_data(context)
    assert field in data, f"Field '{field}' not in JSON data: {list(data.keys())}"
    assert data[field] is True, f"Expected data['{field}'] == True, got {data[field]!r}"


# ---------------------------------------------------------------------------
# Then — JSON origin
# ---------------------------------------------------------------------------


@then('the spec config get JSON origin should have field "{field}"')
def step_spec_json_origin_has_field(context: Context, field: str) -> None:
    data = _get_spec_json_data(context)
    origin = data.get("origin", {})
    assert field in origin, f"Field '{field}' not in JSON origin: {list(origin.keys())}"


# ---------------------------------------------------------------------------
# Then — JSON resolution chain
# ---------------------------------------------------------------------------


@then(
    "the spec config get JSON resolution chain should use human-readable source names"
)
def step_spec_json_chain_human_readable(context: Context) -> None:
    data = _get_spec_json_data(context)
    chain = data.get("resolution_chain", [])
    assert len(chain) > 0, "Expected non-empty resolution chain"
    for entry in chain:
        src = entry.get("source", "")
        assert src in _SPEC_SOURCE_NAMES, (
            f"Source '{src}' is not a spec-required human-readable name. "
            f"Expected one of: {_SPEC_SOURCE_NAMES}"
        )


# ---------------------------------------------------------------------------
# Then — JSON winner
# ---------------------------------------------------------------------------


@then('the spec config get JSON winner should have field "{field}"')
def step_spec_json_winner_has_field(context: Context, field: str) -> None:
    data = _get_spec_json_data(context)
    winner = data.get("winner", {})
    assert field in winner, f"Field '{field}' not in JSON winner: {list(winner.keys())}"


# ---------------------------------------------------------------------------
# Then — JSON timing
# ---------------------------------------------------------------------------


@then('the spec config get JSON timing should have field "{field}"')
def step_spec_json_timing_has_field(context: Context, field: str) -> None:
    envelope = _get_spec_json(context)
    timing = envelope.get("timing", {})
    assert field in timing, f"Field '{field}' not in JSON timing: {list(timing.keys())}"
