"""Step definitions for config_project_scope.feature."""

from __future__ import annotations

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

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

from cleveragents.application.services.config_service import ConfigService
from cleveragents.cli.commands import config as config_mod
from cleveragents.cli.commands.config import app as config_app

_runner = CliRunner()


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


def _get_service(context: Context) -> ConfigService:
    """Return the test-scoped ConfigService instance."""
    svc: ConfigService = context._ps_config_service
    return svc


def _cleanup(context: Context) -> None:
    """Clean up temp dir."""
    tmpdir: str | None = getattr(context, "_ps_config_tmpdir", None)
    if tmpdir and os.path.isdir(tmpdir):
        shutil.rmtree(tmpdir, ignore_errors=True)


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


@given("a clean project-scoped config environment")
def step_clean_project_scoped_env(context: Context) -> None:
    """Set up an isolated temp config directory for project-scoped tests."""
    tmpdir = tempfile.mkdtemp()
    context._ps_config_tmpdir = tmpdir
    tmp_path = Path(tmpdir)
    context._ps_config_service = ConfigService(
        config_dir=tmp_path,
        config_path=tmp_path / "config.toml",
    )
    context._ps_config_dir = tmp_path
    context._ps_config_path = tmp_path / "config.toml"
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(lambda: _cleanup(context))


# ---------------------------------------------------------------------------
# Given
# ---------------------------------------------------------------------------


@given('a global config value "{key}" = "{value}"')
def step_set_global_config(context: Context, key: str, value: str) -> None:
    svc = _get_service(context)
    data = svc.read_config()
    data[key] = value
    svc.write_config(data)


@given('a project-scoped config value "{key}" = "{value}" for project "{project}"')
def step_set_project_config(
    context: Context, key: str, value: str, project: str
) -> None:
    svc = _get_service(context)
    data = svc.read_config()
    proj_section: dict[str, Any] = data.get("project", {})
    if not isinstance(proj_section, dict):
        proj_section = {}
    proj_overrides: dict[str, Any] = proj_section.get(project, {})
    if not isinstance(proj_overrides, dict):
        proj_overrides = {}
    proj_overrides[key] = value
    proj_section[project] = proj_overrides
    data["project"] = proj_section
    svc.write_config(data)


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


@when('I set config "{key}" to "{value}" for project "{project}"')
def step_set_config_for_project(
    context: Context, key: str, value: str, project: str
) -> None:
    svc = _get_service(context)
    data = svc.read_config()
    proj_section: dict[str, Any] = data.get("project", {})
    if not isinstance(proj_section, dict):
        proj_section = {}
    proj_overrides: dict[str, Any] = proj_section.get(project, {})
    if not isinstance(proj_overrides, dict):
        proj_overrides = {}
    proj_overrides[key] = value
    proj_section[project] = proj_overrides
    data["project"] = proj_section
    svc.write_config(data)


@when('I get config "{key}" for project "{project}"')
def step_get_config_for_project(context: Context, key: str, project: str) -> None:
    svc = _get_service(context)
    context.resolved = svc.resolve(key, project_name=project)


@when('I list config for project "{project}"')
def step_list_config_for_project(context: Context, project: str) -> None:
    svc = _get_service(context)
    context.project_overrides = svc.get_project_overrides(project)


@when('I attempt to set non-scopable key "{key}" to "{value}" for project "{project}"')
def step_attempt_set_non_scopable(
    context: Context, key: str, value: str, project: str
) -> None:
    svc = _get_service(context)
    try:
        svc.set_project_value(project, key, value)
        context.raised_error = None
    except (ValueError, TypeError) as exc:
        context.raised_error = exc


@when('I run config set "{key}" "{value}" with project "{project}" via CLI')
def step_run_cli_set_with_project(
    context: Context, key: str, value: str, project: str
) -> None:
    tmp_dir: Path = context._ps_config_dir
    tmp_path: Path = context._ps_config_path
    with (
        patch.object(config_mod, "_CONFIG_DIR", tmp_dir),
        patch.object(config_mod, "_CONFIG_PATH", tmp_path),
    ):
        context.result = _runner.invoke(
            config_app, ["set", key, value, "--project", project]
        )


@when('I run config get "{key}" with project "{project}" via CLI')
def step_run_cli_get_with_project(context: Context, key: str, project: str) -> None:
    tmp_dir: Path = context._ps_config_dir
    tmp_path: Path = context._ps_config_path
    with (
        patch.object(config_mod, "_CONFIG_DIR", tmp_dir),
        patch.object(config_mod, "_CONFIG_PATH", tmp_path),
    ):
        context.result = _runner.invoke(
            config_app, ["get", key, "--project", project, "--format", "json"]
        )


@when('I run config list with project "{project}" via CLI')
def step_run_cli_list_with_project(context: Context, project: str) -> None:
    tmp_dir: Path = context._ps_config_dir
    tmp_path: Path = context._ps_config_path
    with (
        patch.object(config_mod, "_CONFIG_DIR", tmp_dir),
        patch.object(config_mod, "_CONFIG_PATH", tmp_path),
    ):
        context.result = _runner.invoke(
            config_app, ["list", "--project", project, "--format", "json"]
        )


@when("I attempt to get overrides for empty project name")
def step_attempt_empty_project_name(context: Context) -> None:
    svc = _get_service(context)
    try:
        svc.get_project_overrides("")
        context.raised_error = None
    except ValueError as exc:
        context.raised_error = exc


@when('I use service set_project_value for "{project}" key "{key}" value "{value}"')
def step_use_service_set_project_value(
    context: Context, project: str, key: str, value: str
) -> None:
    svc = _get_service(context)
    svc.set_project_value(project, key, value)


@when('I get all project overrides for "{project}"')
def step_get_all_project_overrides(context: Context, project: str) -> None:
    svc = _get_service(context)
    context.project_overrides = svc.get_project_overrides(project)


# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------


@then('the project-scoped config for "{project}" should contain "{key}" = "{value}"')
def step_project_config_contains(
    context: Context, project: str, key: str, value: str
) -> None:
    svc = _get_service(context)
    overrides = svc.get_project_overrides(project)
    assert key in overrides, (
        f"Expected key '{key}' in project overrides, got: {list(overrides.keys())}"
    )
    assert str(overrides[key]) == value, f"Expected '{value}', got '{overrides[key]}'"


@then('the project-resolved value should be "{value}"')
def step_project_resolved_value(context: Context, value: str) -> None:
    assert str(context.resolved.value) == value, (
        f"Expected '{value}', got '{context.resolved.value}'"
    )


@then("the output should show {count:d} project-scoped overrides")
def step_project_override_count(context: Context, count: int) -> None:
    overrides: dict[str, Any] = context.project_overrides
    assert len(overrides) == count, (
        f"Expected {count} overrides, got {len(overrides)}: {list(overrides.keys())}"
    )


@then("the project set should fail with a scopable error")
def step_project_set_fail_scopable(context: Context) -> None:
    assert context.raised_error is not None, "Expected ValueError but none was raised"
    assert "not project-scopable" in str(context.raised_error), (
        f"Expected 'not project-scopable' in: {context.raised_error}"
    )


@then("the CLI config set should succeed")
def step_cli_set_succeed(context: Context) -> None:
    assert context.result.exit_code == 0, (
        f"Expected exit 0, got {context.result.exit_code}: {context.result.output}"
    )


@then('the CLI output should mention project scope "{project}"')
def step_cli_output_mentions_project(context: Context, project: str) -> None:
    output: str = context.result.output
    assert project in output, f"Expected '{project}' in output: {output}"


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


@then("the CLI config list should succeed")
def step_cli_list_succeed(context: Context) -> None:
    assert context.result.exit_code == 0, (
        f"Expected exit 0, got {context.result.exit_code}: {context.result.output}"
    )


@then("the CLI list output should show exactly {count:d} entries")
def step_cli_list_exact_count(context: Context, count: int) -> None:
    output: str = context.result.output
    data: list[dict[str, Any]] = json.loads(output)
    assert len(data) == count, f"Expected {count} entries, got {len(data)}: {data}"


@then("the CLI config list should show no overrides message")
def step_cli_list_no_overrides(context: Context) -> None:
    output: str = context.result.output
    # When no overrides and using JSON format, an empty or message output
    # The command should still succeed (exit 0) but indicate nothing found
    assert context.result.exit_code == 0 or "No project-scoped overrides" in output, (
        f"Expected no-overrides output, got: {output}"
    )


@then("the empty project name should raise ValueError")
def step_empty_project_raises(context: Context) -> None:
    assert context.raised_error is not None, "Expected ValueError but none was raised"
    assert isinstance(context.raised_error, ValueError), (
        f"Expected ValueError, got {type(context.raised_error).__name__}"
    )


@then("the project overrides should have {count:d} keys")
def step_project_overrides_key_count(context: Context, count: int) -> None:
    overrides: dict[str, Any] = context.project_overrides
    assert len(overrides) == count, (
        f"Expected {count} keys, got {len(overrides)}: {list(overrides.keys())}"
    )


@then('the project overrides should not contain "{key}"')
def step_project_overrides_not_contain(context: Context, key: str) -> None:
    overrides: dict[str, Any] = context.project_overrides
    assert key not in overrides, (
        f"Expected '{key}' NOT in project overrides, but it was present"
    )
