# pyright: reportRedeclaration=false
"""Step definitions for actor context remove/export/import commands."""

from __future__ import annotations

import json
import shutil
import tempfile
from pathlib import Path
from typing import Any

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

from cleveragents.cli.commands.actor_context import app as actor_context_app
from cleveragents.reactive.context_manager import ContextManager

_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


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


@given("a temporary context directory for actor context tests")
def step_temp_context_dir(context):
    context.tmp_dir = Path(tempfile.mkdtemp())
    context.context_dir = context.tmp_dir / "contexts"
    context.context_dir.mkdir(parents=True, exist_ok=True)
    context.runner = CliRunner()
    context._cleanup_handlers: list = getattr(context, "_cleanup_handlers", [])
    context._cleanup_handlers.append(
        lambda: shutil.rmtree(context.tmp_dir, ignore_errors=True)
    )
    # Will store original messages for round-trip checks
    context.original_messages = None
    context.export_file = None


# ---------------------------------------------------------------------------
# Givens — context creation helpers
# ---------------------------------------------------------------------------


@given('an actor context named "{name}" exists')
def step_context_exists(context, name):
    mgr = ContextManager(name, context.context_dir)
    mgr.add_message("system", f"Initial message for {name}")


@given('an actor context named "{name}" exists with messages')
def step_context_exists_with_messages(context, name):
    mgr = ContextManager(name, context.context_dir)
    mgr.add_message("user", "Hello, how are you?")
    mgr.add_message("assistant", "I'm doing well, thanks for asking!")
    mgr.add_message("user", "Great, let's get started.")
    context.original_messages = list(mgr.messages)


@given('a valid context JSON file named "{filename}"')
def step_create_json_file(context, filename):
    data = {
        "context_name": filename.replace(".json", ""),
        "messages": [
            {
                "role": "user",
                "content": "test message",
                "timestamp": "2026-01-01T00:00:00",
                "metadata": {},
            },
        ],
        "metadata": {"context_name": filename.replace(".json", "")},
        "state": {},
        "global_context": {},
    }
    context.import_file = context.tmp_dir / filename
    context.import_file.write_text(json.dumps(data, indent=2), encoding="utf-8")


@given('a valid context JSON file with context_name "{name}"')
def step_create_json_file_with_name(context, name):
    data = {
        "context_name": name,
        "messages": [
            {
                "role": "user",
                "content": "auto-named test",
                "timestamp": "2026-01-01T00:00:00",
                "metadata": {},
            },
        ],
        "metadata": {"context_name": name},
        "state": {},
        "global_context": {},
    }
    context.import_file = context.tmp_dir / f"{name}.json"
    context.import_file.write_text(json.dumps(data, indent=2), encoding="utf-8")


# ---------------------------------------------------------------------------
# When — remove
# ---------------------------------------------------------------------------


@when('I run actor context remove "{name}" with --yes')
def step_remove_named_yes(context, name):
    context.result = context.runner.invoke(
        actor_context_app,
        ["remove", name, "--yes", "--context-dir", str(context.context_dir)],
    )


@when("I run actor context remove --all with --yes")
def step_remove_all_yes(context):
    context.result = context.runner.invoke(
        actor_context_app,
        ["remove", "--all", "--yes", "--context-dir", str(context.context_dir)],
    )


@when('I run actor context remove "{name}" with --yes and format "{fmt}"')
def step_remove_named_format(context, name, fmt):
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "remove",
            name,
            "--yes",
            "--context-dir",
            str(context.context_dir),
            "--format",
            fmt,
        ],
    )


@when("I run actor context remove without name or all")
def step_remove_no_args(context):
    context.result = context.runner.invoke(
        actor_context_app,
        ["remove", "--context-dir", str(context.context_dir)],
    )


@when('I run actor context remove "{name}" with --all')
def step_remove_name_and_all(context, name):
    context.result = context.runner.invoke(
        actor_context_app,
        ["remove", name, "--all", "--context-dir", str(context.context_dir)],
    )


# ---------------------------------------------------------------------------
# When — export
# ---------------------------------------------------------------------------


@when('I run actor context export "{name}" to a JSON file')
def step_export_json(context, name):
    context.export_file = context.tmp_dir / f"{name}-export.json"
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "export",
            name,
            "--output",
            str(context.export_file),
            "--context-dir",
            str(context.context_dir),
        ],
    )


@when('I run actor context export "{name}" to a YAML file')
def step_export_yaml(context, name):
    context.export_file = context.tmp_dir / f"{name}-export.yaml"
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "export",
            name,
            "--output",
            str(context.export_file),
            "--context-dir",
            str(context.context_dir),
        ],
    )


@when('I run actor context export "{name}" to a JSON file with format "{fmt}"')
def step_export_json_format(context, name, fmt):
    context.export_file = context.tmp_dir / f"{name}-export.json"
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "export",
            name,
            "--output",
            str(context.export_file),
            "--context-dir",
            str(context.context_dir),
            "--format",
            fmt,
        ],
    )


# ---------------------------------------------------------------------------
# When — import
# ---------------------------------------------------------------------------


@when('I run actor context import from that file as "{name}"')
def step_import_named(context, name):
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "import",
            name,
            "--input",
            str(context.import_file),
            "--context-dir",
            str(context.context_dir),
        ],
    )


@when("I run actor context import from that file without a name")
def step_import_no_name(context):
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "import",
            "--input",
            str(context.import_file),
            "--context-dir",
            str(context.context_dir),
        ],
    )


@when('I run actor context import from that file as "{name}" without update')
def step_import_no_update(context, name):
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "import",
            name,
            "--input",
            str(context.import_file),
            "--context-dir",
            str(context.context_dir),
        ],
    )


@when('I run actor context import from that file as "{name}" with --update')
def step_import_with_update(context, name):
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "import",
            name,
            "--input",
            str(context.import_file),
            "--update",
            "--context-dir",
            str(context.context_dir),
        ],
    )


# ---------------------------------------------------------------------------
# When — round-trip helpers
# ---------------------------------------------------------------------------


@when('I export the context "{name}" to a JSON file')
def step_roundtrip_export(context, name):
    context.export_file = context.tmp_dir / f"{name}-roundtrip.json"
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "export",
            name,
            "--output",
            str(context.export_file),
            "--context-dir",
            str(context.context_dir),
        ],
    )
    assert context.result.exit_code == 0, context.result.output


@when('I remove the context "{name}"')
def step_roundtrip_remove(context, name):
    context.result = context.runner.invoke(
        actor_context_app,
        ["remove", name, "--yes", "--context-dir", str(context.context_dir)],
    )
    assert context.result.exit_code == 0, context.result.output


@when('I import the context from that JSON file as "{name}"')
def step_roundtrip_import(context, name):
    context.result = context.runner.invoke(
        actor_context_app,
        [
            "import",
            name,
            "--input",
            str(context.export_file),
            "--context-dir",
            str(context.context_dir),
        ],
    )
    assert context.result.exit_code == 0, context.result.output


# ---------------------------------------------------------------------------
# Then — success / failure
# ---------------------------------------------------------------------------


@then("the actor context remove command should succeed")
def step_remove_success(context):
    assert context.result.exit_code == 0, (
        f"Expected exit 0, got {context.result.exit_code}.\n"
        f"stdout: {context.result.output}\n"
        f"stderr: {getattr(context.result, 'stderr', '')}"
    )


@then("the actor context export command should succeed")
def step_export_success(context):
    assert context.result.exit_code == 0, (
        f"Expected exit 0, got {context.result.exit_code}.\n"
        f"stdout: {context.result.output}\n"
        f"stderr: {getattr(context.result, 'stderr', '')}"
    )


@then("the actor context import command should succeed")
def step_import_success(context):
    assert context.result.exit_code == 0, (
        f"Expected exit 0, got {context.result.exit_code}.\n"
        f"stdout: {context.result.output}\n"
        f"stderr: {getattr(context.result, 'stderr', '')}"
    )


@then("the actor context remove command should fail with exit code 1")
def step_remove_fail(context):
    assert context.result.exit_code == 1, (
        f"Expected exit 1, got {context.result.exit_code}.\n"
        f"stdout: {context.result.output}\n"
        f"stderr: {getattr(context.result, 'stderr', '')}"
    )


@then("the actor context export command should fail with exit code 1")
def step_export_fail(context):
    assert context.result.exit_code == 1, (
        f"Expected exit 1, got {context.result.exit_code}.\n"
        f"stdout: {context.result.output}\n"
        f"stderr: {getattr(context.result, 'stderr', '')}"
    )


@then("the actor context import command should fail with exit code 1")
def step_import_fail(context):
    assert context.result.exit_code == 1, (
        f"Expected exit 1, got {context.result.exit_code}.\n"
        f"stdout: {context.result.output}\n"
        f"stderr: {getattr(context.result, 'stderr', '')}"
    )


# ---------------------------------------------------------------------------
# Then — state assertions
# ---------------------------------------------------------------------------


@then('the context "{name}" should no longer exist')
def step_context_gone(context, name):
    ctx_path = context.context_dir / name
    assert not ctx_path.exists(), f"Context dir {ctx_path} still exists"


@then("no actor contexts should remain")
def step_no_contexts(context):
    remaining = [d for d in context.context_dir.iterdir() if d.is_dir()]
    assert len(remaining) == 0, (
        f"Expected 0 contexts, found {len(remaining)}: {remaining}"
    )


@then('the context "{name}" should exist')
def step_context_exists_check(context, name):
    mgr = ContextManager(name, context.context_dir)
    assert mgr.exists(), f"Context '{name}' does not exist at {mgr.context_dir}"


# ---------------------------------------------------------------------------
# Then — output assertions
# ---------------------------------------------------------------------------


@then('the output should contain valid JSON with key "{key}"')
def step_output_json_key(context, key):
    output = context.result.output
    parsed = json.loads(output)
    data = _unwrap_envelope(parsed)
    assert key in data, f"Key '{key}' not found in JSON output: {data.keys()}"


@then("the exported file should exist and contain valid JSON")
def step_exported_json_valid(context):
    assert context.export_file.exists(), f"Export file {context.export_file} not found"
    data = json.loads(context.export_file.read_text(encoding="utf-8"))
    assert isinstance(data, dict)


@then('the exported JSON should contain key "{key}"')
def step_exported_json_key(context, key):
    data = json.loads(context.export_file.read_text(encoding="utf-8"))
    assert key in data, f"Key '{key}' not found in exported JSON: {data.keys()}"


@then("the exported YAML file should exist and be valid")
def step_exported_yaml_valid(context):
    import yaml

    assert context.export_file.exists(), f"Export file {context.export_file} not found"
    data = yaml.safe_load(context.export_file.read_text(encoding="utf-8"))
    assert isinstance(data, dict)


@then("the imported context should have the same messages as the original")
def step_roundtrip_messages(context):
    assert context.original_messages is not None, "No original messages saved"
    mgr = ContextManager("roundtrip", context.context_dir)
    imported = mgr.messages

    # Compare content and role, ignoring timestamp differences
    assert len(imported) == len(context.original_messages), (
        f"Message count mismatch: {len(imported)} vs {len(context.original_messages)}"
    )
    for orig, imp in zip(context.original_messages, imported, strict=True):
        assert orig["role"] == imp["role"], (
            f"Role mismatch: {orig['role']} vs {imp['role']}"
        )
        assert orig["content"] == imp["content"], (
            f"Content mismatch: {orig['content']!r} vs {imp['content']!r}"
        )
