"""Step definitions for tdd_plan_correct_json_output.feature.

Captures bug #8584: the ``plan correct`` CLI command's JSON output
(``--format json``) does not match the spec-required envelope structure.

The current implementation places correction fields directly under ``data``
(``data.mode``, ``data.correction_id``) instead of nesting them under
``data.correction``.  This means ``data.correction.mode`` is absent from
the output.  Additionally, the ``command`` field is empty instead of
``"plan correct"``.

The assertions verify the *expected* (correct) behaviour.  They will
**fail** on the current codebase, proving the bug exists.  The
``@tdd_expected_fail`` tag inverts the result so CI passes.

All step text uses the ``tpcjo`` prefix to avoid collisions with other
step files.

Bug: https://git.cleverthis.com/cleveragents/cleveragents-core/issues/8584
"""

from __future__ import annotations

import json
from unittest.mock import patch

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

from features.mocks.tdd_plan_correct_json_output_fixtures import (
    PATCH_CONTAINER,
    build_cli_args,
    make_container,
)

runner = CliRunner()


def _get_plan_app():
    """Lazily import the plan CLI app to avoid slow module-level import.

    ``cleveragents.cli.commands.plan`` triggers a large import chain
    (application container, all services, etc.) that takes ~100 s on a
    cold interpreter.  Deferring the import to the first step execution
    means the module is already cached by the time these scenarios run,
    so the actual cost is near-zero.
    """
    from cleveragents.cli.commands.plan import app as plan_app

    return plan_app


# ---------------------------------------------------------------------------
# GIVEN steps
# ---------------------------------------------------------------------------


@given(
    "tpcjo a container with a plan and a correction service"
    " that succeeds in revert mode"
)
def step_tpcjo_container_revert(context: Context) -> None:
    """Set up a mock DI container for revert mode."""
    context.tpcjo_container = make_container(mode="revert")
    context.tpcjo_mode = "revert"


@given(
    "tpcjo a container with a plan and a correction service"
    " that succeeds in append mode"
)
def step_tpcjo_container_append(context: Context) -> None:
    """Set up a mock DI container for append mode."""
    context.tpcjo_container = make_container(mode="append")
    context.tpcjo_mode = "append"


# ---------------------------------------------------------------------------
# WHEN steps
# ---------------------------------------------------------------------------


@when("tpcjo I invoke plan correct with --format json in revert mode")
def step_tpcjo_invoke_revert(context: Context) -> None:
    """Invoke ``plan correct <decision_id> --format json --mode revert``.

    Patches ``get_container`` so the command uses the mock DI container
    instead of the real application container.
    """
    args = build_cli_args(mode="revert")
    with patch(PATCH_CONTAINER, return_value=context.tpcjo_container):
        context.tpcjo_result = runner.invoke(_get_plan_app(), args)
    _parse_json_output(context)


@when("tpcjo I invoke plan correct with --format json in append mode")
def step_tpcjo_invoke_append(context: Context) -> None:
    """Invoke ``plan correct <decision_id> --format json --mode append``.

    Patches ``get_container`` so the command uses the mock DI container
    instead of the real application container.
    """
    args = build_cli_args(mode="append")
    with patch(PATCH_CONTAINER, return_value=context.tpcjo_container):
        context.tpcjo_result = runner.invoke(_get_plan_app(), args)
    _parse_json_output(context)


# ---------------------------------------------------------------------------
# THEN steps
# ---------------------------------------------------------------------------


@then('tpcjo the JSON output data.correction.mode should be "{expected_mode}"')
def step_tpcjo_json_correction_mode(context: Context, expected_mode: str) -> None:
    """Assert the parsed JSON output has data.correction.mode set correctly.

    This assertion will FAIL on the current codebase because the
    implementation places ``mode`` directly under ``data`` (as ``data.mode``)
    instead of nesting it under ``data.correction`` (as ``data.correction.mode``).
    """
    parsed = context.tpcjo_parsed_json
    assert "data" in parsed, (
        f"Bug #8584: JSON output is missing top-level 'data' key.  "
        f"Current output keys: {list(parsed.keys())}.  "
        f"Full output: {context.tpcjo_result.output!r}"
    )
    data = parsed["data"]
    assert isinstance(data, dict), (
        f"Bug #8584: JSON output 'data' value is not a dict: {data!r}"
    )
    assert "correction" in data, (
        f"Bug #8584: JSON output 'data' is missing 'correction' key.  "
        f"Current data keys: {list(data.keys())}.  "
        f"The spec requires correction fields to be nested under data.correction, "
        f"but the current implementation places them directly under data "
        f"(data.mode, data.correction_id, etc.)."
    )
    correction = data["correction"]
    assert isinstance(correction, dict), (
        f"Bug #8584: JSON output 'data.correction' is not a dict: {correction!r}"
    )
    assert "mode" in correction, (
        f"Bug #8584: JSON output 'data.correction' is missing 'mode' key.  "
        f"Current correction keys: {list(correction.keys())}"
    )
    actual_mode = correction["mode"]
    assert actual_mode == expected_mode, (
        f"Bug #8584: data.correction.mode is {actual_mode!r}, "
        f"expected {expected_mode!r}"
    )


@then('tpcjo the JSON output command field should be "{expected_command}"')
def step_tpcjo_json_command_field(context: Context, expected_command: str) -> None:
    """Assert the parsed JSON output has the correct command field value.

    This assertion will FAIL on the current codebase because the
    ``command`` field is an empty string instead of ``"plan correct"``.
    """
    parsed = context.tpcjo_parsed_json
    assert "command" in parsed, (
        f"Bug #8584: JSON output is missing top-level 'command' key.  "
        f"Current output keys: {list(parsed.keys())}.  "
        f"Full output: {context.tpcjo_result.output!r}"
    )
    actual_command = parsed["command"]
    assert actual_command == expected_command, (
        f"Bug #8584: command field is {actual_command!r}, "
        f"expected {expected_command!r}.  "
        f"The spec requires command to be 'plan correct' but the current "
        f"implementation sets it to an empty string."
    )


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


def _parse_json_output(context: Context) -> None:
    """Parse the CLI output as JSON and store it on the context.

    If the command failed or the output is not valid JSON, stores an
    empty dict so that subsequent assertions produce clear failure messages.
    """
    result = context.tpcjo_result
    try:
        context.tpcjo_parsed_json = json.loads(result.output)
    except (json.JSONDecodeError, ValueError):
        context.tpcjo_parsed_json = {}
