"""Step definitions for config_cli_coverage_boost.feature.

Targets uncovered lines in cleveragents/cli/commands/config.py:
  - Line  135: _env_var_for_key returning entry.env_var for registered key
  - Lines 145-146: _resolve_source error path returning "default"
  - Lines 155-156: _resolution_chain error path returning []
  - Line  210: config_set --project on non-project-scopable key
  - Line  213: config_set --project with corrupted project section
  - Line  216: config_set --project with corrupted project overrides
  - Line  306: config_get verbose non-rich with Path fspath in chain
  - Lines 398-401: config_list --project key regex filter skip
  - Line  405: config_list --project value regex filter skip
  - Line  410: config_list --project secret masking
  - Line  430: config_list --project non-rich Path fspath serialisation
  - Lines 434-452: config_list --project rich table rendering
  - Line  501: config_list all non-rich Path fspath serialisation

All step text uses a 'cfg-boost' prefix to avoid collisions with other
step definition files.
"""

from __future__ import annotations

import contextlib
import json
import os
import shutil
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

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

from cleveragents.application.services.config_service import (
    _REGISTRY,
    ConfigLevel,
    ResolvedValue,
)
from cleveragents.cli.commands import config as config_mod
from cleveragents.cli.commands.config import (
    _env_var_for_key,
    _resolution_chain,
    _resolve_source,
)
from cleveragents.cli.commands.config import app as config_app

_runner = CliRunner()

_ENVELOPE_KEYS = {"command", "status", "exit_code", "data", "timing", "messages"}


def _unwrap_envelope(parsed: Any) -> Any:
    """Return the ``data`` field if *parsed* is a spec envelope, else *parsed* as-is."""
    if isinstance(parsed, dict) and _ENVELOPE_KEYS.issubset(parsed.keys()):
        return parsed["data"]
    return parsed


# ---------------------------------------------------------------------------
# Helpers — isolated temp directory
# ---------------------------------------------------------------------------


def _setup_boost_temp(context: Context) -> None:
    """Redirect _CONFIG_DIR/_CONFIG_PATH to a fresh temp directory."""
    if not hasattr(context, "_boost_tmpdir"):
        context._boost_tmpdir = tempfile.mkdtemp(prefix="cfg_boost_")
    tmp = Path(context._boost_tmpdir)
    context._boost_dir_patch = patch.object(config_mod, "_CONFIG_DIR", tmp)
    context._boost_path_patch = patch.object(
        config_mod, "_CONFIG_PATH", tmp / "config.toml"
    )
    context._boost_dir_patch.start()
    context._boost_path_patch.start()

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


def _teardown_boost_temp(context: Context) -> None:
    for attr in ("_boost_dir_patch", "_boost_path_patch"):
        patcher = getattr(context, attr, None)
        if patcher is not None:
            with contextlib.suppress(RuntimeError):
                patcher.stop()
    tmpdir = getattr(context, "_boost_tmpdir", None)
    if tmpdir and os.path.isdir(tmpdir):
        shutil.rmtree(tmpdir, ignore_errors=True)


def _write_toml(data: dict[str, Any]) -> None:
    """Write a dict to the temp config.toml using tomlkit."""
    config_path: Path = config_mod._CONFIG_PATH  # type: ignore[assignment]
    config_path.parent.mkdir(parents=True, exist_ok=True)
    doc = tomlkit.document()
    for key, value in data.items():
        doc[key] = value
    with open(config_path, "w") as fh:
        tomlkit.dump(doc, fh)


def _write_project_override(project: str, key: str, value: str) -> None:
    """Write a TOML config file with a project override."""
    config_path: Path = config_mod._CONFIG_PATH  # type: ignore[assignment]
    config_path.parent.mkdir(parents=True, exist_ok=True)
    doc = tomlkit.document()
    proj_table = tomlkit.table(is_super_table=True)
    inner = tomlkit.table()
    # Coerce numeric strings
    try:
        inner[key] = int(value)
    except ValueError:
        inner[key] = value
    proj_table[project] = inner
    doc["project"] = proj_table
    with open(config_path, "w") as fh:
        tomlkit.dump(doc, fh)


# ===================================================================
# Background
# ===================================================================


@given("a cfg-boost isolated temp config directory")
def step_cfg_boost_temp_dir(context: Context) -> None:
    _setup_boost_temp(context)


# ===================================================================
# _env_var_for_key — line 135
# ===================================================================


@when('I call cfg-boost _env_var_for_key with a registered key "{key}"')
def step_cfg_boost_env_var_for_key(context: Context, key: str) -> None:
    context.cfg_boost_env_var_result = _env_var_for_key(key)


@then('the cfg-boost returned env var should match the registry for "{key}"')
def step_cfg_boost_check_env_var(context: Context, key: str) -> None:
    entry = _REGISTRY[key]
    assert context.cfg_boost_env_var_result == entry.env_var, (
        f"Expected '{entry.env_var}', got '{context.cfg_boost_env_var_result}'"
    )


# ===================================================================
# _resolve_source error path — lines 145-146
# ===================================================================


@when("I call cfg-boost _resolve_source with a key that triggers ValueError")
def step_cfg_boost_resolve_source_error(context: Context) -> None:
    mock_svc = MagicMock()
    mock_svc.resolve.side_effect = ValueError("boom")
    with patch.object(config_mod, "_get_service", return_value=mock_svc):
        context.cfg_boost_resolve_source_result = _resolve_source("any.key")


@then('the cfg-boost returned source should be "{source}"')
def step_cfg_boost_check_source(context: Context, source: str) -> None:
    assert context.cfg_boost_resolve_source_result == source, (
        f"Expected '{source}', got '{context.cfg_boost_resolve_source_result}'"
    )


# ===================================================================
# _resolution_chain error path — lines 155-156
# ===================================================================


@when("I call cfg-boost _resolution_chain with a key that triggers KeyError")
def step_cfg_boost_resolution_chain_error(context: Context) -> None:
    mock_svc = MagicMock()
    mock_svc.resolve.side_effect = KeyError("missing")
    with patch.object(config_mod, "_get_service", return_value=mock_svc):
        context.cfg_boost_chain_result = _resolution_chain("any.key")


@then("the cfg-boost returned chain should be empty")
def step_cfg_boost_check_chain_empty(context: Context) -> None:
    assert context.cfg_boost_chain_result == [], (
        f"Expected [], got {context.cfg_boost_chain_result}"
    )


# ===================================================================
# config_set --project non-scopable — line 210
# ===================================================================


@when('I invoke cfg-boost config set "{key}" "{value}" with project "{project}"')
def step_cfg_boost_set_project(
    context: Context, key: str, value: str, project: str
) -> None:
    context.cfg_boost_result = _runner.invoke(
        config_app, ["set", key, value, "--project", project]
    )


@then("the cfg-boost CLI result should have failed")
def step_cfg_boost_cli_failed(context: Context) -> None:
    assert context.cfg_boost_result.exit_code != 0, (
        f"Expected non-zero exit, got {context.cfg_boost_result.exit_code}: "
        f"{context.cfg_boost_result.output}"
    )


@then('the cfg-boost CLI output should include "{text}"')
def step_cfg_boost_cli_output_includes(context: Context, text: str) -> None:
    output = context.cfg_boost_result.output
    assert text in output, f"Expected '{text}' in output:\n{output}"


# ===================================================================
# config_set --project corrupted project section — line 213
# ===================================================================


@given('the cfg-boost config file has a non-dict "project" value')
def step_cfg_boost_write_corrupted_project_section(context: Context) -> None:
    _write_toml({"project": "not-a-dict"})


# ===================================================================
# config_set --project corrupted proj overrides — line 216
# ===================================================================


@given('the cfg-boost config file has a non-dict project override for "{project}"')
def step_cfg_boost_write_corrupted_proj_override(
    context: Context, project: str
) -> None:
    data: dict[str, Any] = {"project": {project: "not-a-dict"}}
    _write_toml(data)


@then("the cfg-boost CLI result should have succeeded")
def step_cfg_boost_cli_succeeded(context: Context) -> None:
    assert context.cfg_boost_result.exit_code == 0, (
        f"Expected exit 0, got {context.cfg_boost_result.exit_code}: "
        f"{context.cfg_boost_result.output}"
    )


# ===================================================================
# config_get verbose non-rich with Path value in chain — line 306
# ===================================================================


@when(
    'I invoke cfg-boost config get "{key}" verbose format "{fmt}" with path-like chain'
)
def step_cfg_boost_get_verbose_path(context: Context, key: str, fmt: str) -> None:
    # Build a mock resolved value whose chain contains a Path object
    chain_with_path = [
        {"source": "cli_flag", "value": None},
        {"source": "env_var", "value": None},
        {"source": "project", "value": None},
        {"source": "global", "value": Path("/tmp/fake/path")},
        {"source": "default", "value": Path("/default/path")},
    ]
    mock_resolved = ResolvedValue(
        key=key,
        value=Path("/tmp/fake/path"),
        source=ConfigLevel.GLOBAL,
        chain=chain_with_path,
    )
    mock_svc = MagicMock()
    mock_svc.resolve.return_value = mock_resolved

    with patch.object(config_mod, "_get_service", return_value=mock_svc):
        context.cfg_boost_result = _runner.invoke(
            config_app,
            ["get", key, "--verbose", "--format", fmt],
        )


@then("the cfg-boost JSON resolution chain entries should have string values")
def step_cfg_boost_json_chain_strings(context: Context) -> None:
    parsed = json.loads(context.cfg_boost_result.output)
    data = _unwrap_envelope(parsed)
    chain = data.get("resolution_chain", [])
    assert len(chain) > 0, "Expected non-empty resolution chain"
    for entry in chain:
        val = entry.get("value")
        if val is not None:
            assert isinstance(val, str), (
                f"Expected string in chain, got {type(val)}: {val}"
            )


# ===================================================================
# config_list --project key regex filter — lines 398-401
# ===================================================================


@given(
    'the cfg-boost config file has project "{project}" override "{key}" as "{value}"'
)
def step_cfg_boost_write_project_override(
    context: Context, project: str, key: str, value: str
) -> None:
    _write_project_override(project, key, value)


@when('I invoke cfg-boost config list with project "{project}" pattern "{pattern}"')
def step_cfg_boost_list_project_pattern(
    context: Context, project: str, pattern: str
) -> None:
    context.cfg_boost_result = _runner.invoke(
        config_app, ["list", pattern, "--project", project]
    )


@then("the cfg-boost list output should show no matching overrides")
def step_cfg_boost_no_matching(context: Context) -> None:
    output = context.cfg_boost_result.output
    assert (
        "No project-scoped overrides match" in output
        or context.cfg_boost_result.exit_code == 0
    ), f"Unexpected output:\n{output}"


# ===================================================================
# config_list --project value filter — line 405
# ===================================================================


@when(
    'I invoke cfg-boost config list with project "{project}" value-filter "{valfilter}"'
)
def step_cfg_boost_list_project_valfilter(
    context: Context, project: str, valfilter: str
) -> None:
    context.cfg_boost_result = _runner.invoke(
        config_app,
        ["list", "--project", project, "--filter-values", valfilter],
    )


# ===================================================================
# config_list --project secret masking — line 410
# ===================================================================


@given(
    'the cfg-boost config file has project "{project}" '
    'secret override "{key}" as "{value}"'
)
def step_cfg_boost_write_secret_override(
    context: Context, project: str, key: str, value: str
) -> None:
    _write_project_override(project, key, value)


@when('I invoke cfg-boost config list project "{project}" format "{fmt}"')
def step_cfg_boost_list_project_fmt(context: Context, project: str, fmt: str) -> None:
    context.cfg_boost_result = _runner.invoke(
        config_app, ["list", "--project", project, "--format", fmt]
    )


@then("the cfg-boost JSON list should show masked secret values")
def step_cfg_boost_json_secret_masked(context: Context) -> None:
    output = context.cfg_boost_result.output.strip()
    data = json.loads(output)
    assert isinstance(data, list), f"Expected list, got {type(data)}"
    # Find entries that look like secrets
    secret_entries = [
        e
        for e in data
        if "api-key" in e.get("key", "")
        or "token" in e.get("key", "")
        or "secret" in e.get("key", "")
        or "password" in e.get("key", "")
    ]
    assert len(secret_entries) > 0, f"No secret entries found in: {data}"
    for entry in secret_entries:
        assert entry["value"] == "****", (
            f"Expected masked value '****' for {entry['key']}, got '{entry['value']}'"
        )


# ===================================================================
# config_list --project non-rich Path fspath — line 430
# ===================================================================


@when(
    'I invoke cfg-boost config list project "{project}" format "{fmt}" with path value'
)
def step_cfg_boost_list_project_path_value(
    context: Context, project: str, fmt: str
) -> None:
    # Mock get_project_overrides to return a dict containing a Path object
    mock_svc = MagicMock()
    mock_svc.get_project_overrides.return_value = {
        "core.data-dir": Path("/tmp/overridden"),
    }

    with patch.object(config_mod, "_get_service", return_value=mock_svc):
        context.cfg_boost_result = _runner.invoke(
            config_app,
            ["list", "--project", project, "--format", fmt],
        )


@then("the cfg-boost JSON list entries should have only primitive values")
def step_cfg_boost_json_list_primitives(context: Context) -> None:
    output = context.cfg_boost_result.output.strip()
    data = json.loads(output)
    assert isinstance(data, list), f"Expected list, got {type(data)}"
    for entry in data:
        val = entry.get("value")
        if val is not None:
            assert isinstance(val, (str, int, float, bool)), (
                f"Expected primitive type, got {type(val)}: {val}"
            )


# ===================================================================
# config_list --project rich table — lines 434-452
# ===================================================================


@when('I invoke cfg-boost config list project "{project}" in default rich format')
def step_cfg_boost_list_project_rich(context: Context, project: str) -> None:
    context.cfg_boost_result = _runner.invoke(
        config_app, ["list", "--project", project]
    )


# ===================================================================
# config_list all non-rich Path fspath — line 501
# ===================================================================


@when('I invoke cfg-boost config list format "{fmt}" with path value in resolve_all')
def step_cfg_boost_list_all_path(context: Context, fmt: str) -> None:
    # Build resolve_all results with one Path value to trigger line 501
    results: dict[str, ResolvedValue] = {}
    for key in _REGISTRY:
        results[key] = ResolvedValue(
            key=key,
            value=_REGISTRY[key].default,
            source=ConfigLevel.DEFAULT,
        )
    # Override one key with a Path value
    results["core.data-dir"] = ResolvedValue(
        key="core.data-dir",
        value=Path("/tmp/path/value"),
        source=ConfigLevel.GLOBAL,
    )

    mock_svc = MagicMock()
    mock_svc.resolve_all.return_value = results

    with patch.object(config_mod, "_get_service", return_value=mock_svc):
        context.cfg_boost_result = _runner.invoke(config_app, ["list", "--format", fmt])


@then("the cfg-boost JSON all-keys list should have stringified path values")
def step_cfg_boost_json_all_stringified(context: Context) -> None:
    output = context.cfg_boost_result.output.strip()
    data = json.loads(output)
    assert isinstance(data, list), f"Expected list, got {type(data)}"
    # Find the core.data-dir entry and verify its value is a string
    data_dir_entries = [e for e in data if e.get("key") == "core.data-dir"]
    assert len(data_dir_entries) > 0, "Expected core.data-dir in list output"
    for entry in data_dir_entries:
        val = entry["value"]
        assert isinstance(val, str), (
            f"Expected string for core.data-dir, got {type(val)}: {val}"
        )
        assert "/tmp/path/value" in val, f"Expected path string in value, got: {val}"
