"""Step definitions for CLI JSON/YAML envelope structure tests."""

from __future__ import annotations

import json
from contextlib import redirect_stdout
from io import StringIO

import yaml
from behave import then, when
from behave.runner import Context

from cleveragents.cli.formatting import format_output

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


def _capture_format_output(data, fmt, **kwargs):
    """Call format_output capturing stdout (machine-readable formats write there)."""
    buf = StringIO()
    with redirect_stdout(buf):
        result = format_output(data, fmt, **kwargs)
    return result or buf.getvalue().rstrip("\n")


def _parse_json_envelope(output: str) -> dict:
    parsed = json.loads(output.strip())
    assert isinstance(parsed, dict), f"Expected dict envelope, got {type(parsed)}"
    return parsed


def _parse_yaml_envelope(output: str) -> dict:
    parsed = yaml.safe_load(output.strip())
    assert isinstance(parsed, dict), f"Expected dict envelope, got {type(parsed)}"
    return parsed


# ── Envelope field presence ──────────────────────────────────────────────


@then('the JSON envelope should contain field "{field}"')
def step_json_envelope_has_field(context: Context, field: str) -> None:
    envelope = _parse_json_envelope(context.result.output)
    assert field in envelope, (
        f"Envelope field '{field}' missing. Keys present: {list(envelope.keys())}"
    )


@then('the YAML envelope should contain field "{field}"')
def step_yaml_envelope_has_field(context: Context, field: str) -> None:
    envelope = _parse_yaml_envelope(context.result.output)
    assert field in envelope, (
        f"Envelope field '{field}' missing. Keys present: {list(envelope.keys())}"
    )


# ── Envelope field values ────────────────────────────────────────────────


@then('the JSON envelope status should be "{expected_status}"')
def step_json_envelope_status(context: Context, expected_status: str) -> None:
    envelope = _parse_json_envelope(context.result.output)
    assert envelope["status"] == expected_status, (
        f"Expected status '{expected_status}', got '{envelope['status']}'"
    )


@then("the JSON envelope exit_code should be {expected_code:d}")
def step_json_envelope_exit_code(context: Context, expected_code: int) -> None:
    envelope = _parse_json_envelope(context.result.output)
    assert envelope["exit_code"] == expected_code, (
        f"Expected exit_code {expected_code}, got {envelope['exit_code']}"
    )


@then('the JSON envelope timing should contain "{key}"')
def step_json_envelope_timing_key(context: Context, key: str) -> None:
    envelope = _parse_json_envelope(context.result.output)
    timing = envelope.get("timing")
    assert isinstance(timing, dict), f"Expected timing dict, got {type(timing)}"
    assert key in timing, f"Key '{key}' missing from timing: {list(timing.keys())}"
    assert isinstance(timing[key], (int, float)), (
        f"timing.{key} should be numeric, got {type(timing[key])}"
    )


@then("the JSON envelope messages should be a non-empty list")
def step_json_envelope_messages_list(context: Context) -> None:
    envelope = _parse_json_envelope(context.result.output)
    messages = envelope.get("messages")
    assert isinstance(messages, list), f"Expected messages list, got {type(messages)}"
    assert len(messages) > 0, "messages list should not be empty"
    # Each message should have level and text
    for msg in messages:
        assert "level" in msg, f"Message missing 'level': {msg}"
        assert "text" in msg, f"Message missing 'text': {msg}"


# ── Data field contains actual payload ──────────────────────────────────


@then("the JSON envelope data should contain actor records")
def step_json_envelope_data_actors(context: Context) -> None:
    envelope = _parse_json_envelope(context.result.output)
    data = envelope["data"]
    assert isinstance(data, list), f"Expected data list, got {type(data)}"
    assert len(data) > 0, "data list should not be empty"
    # Each actor record should have namespaced_name
    assert "namespaced_name" in data[0], (
        f"Actor record missing 'namespaced_name': {list(data[0].keys())}"
    )


@then("the YAML envelope data should contain actor records")
def step_yaml_envelope_data_actors(context: Context) -> None:
    envelope = _parse_yaml_envelope(context.result.output)
    data = envelope["data"]
    assert isinstance(data, list), f"Expected data list, got {type(data)}"
    assert len(data) > 0, "data list should not be empty"
    assert "namespaced_name" in data[0], (
        f"Actor record missing 'namespaced_name': {list(data[0].keys())}"
    )


# ── format_output() direct call envelope ────────────────────────────────


@then("the JSON result data field should contain original keys")
def step_json_result_data_original_keys(context: Context) -> None:
    parsed = json.loads(context.format_result)
    data = parsed["data"]
    assert isinstance(data, dict), f"Expected data dict, got {type(data)}"
    # The original dict had "name" and "count" keys
    assert "name" in data, f"'name' missing from data: {list(data.keys())}"
    assert "count" in data, f"'count' missing from data: {list(data.keys())}"


@then("the YAML result data field should contain original keys")
def step_yaml_result_data_original_keys(context: Context) -> None:
    parsed = yaml.safe_load(context.format_result)
    data = parsed["data"]
    assert isinstance(data, dict), f"Expected data dict, got {type(data)}"
    assert "name" in data, f"'name' missing from data: {list(data.keys())}"
    assert "count" in data, f"'count' missing from data: {list(data.keys())}"


@when('I call format_output with command name "{command_name}" and format json')
def step_call_format_output_with_command(context: Context, command_name: str) -> None:
    data = {"name": "test", "count": 42}
    context.format_result = _capture_format_output(data, "json", command=command_name)


@then('the JSON envelope command should be "{expected_command}"')
def step_json_envelope_command(context: Context, expected_command: str) -> None:
    parsed = json.loads(context.format_result)
    assert parsed["command"] == expected_command, (
        f"Expected command '{expected_command}', got '{parsed['command']}'"
    )


@then("the plain result should not contain envelope keys")
def step_plain_no_envelope(context: Context) -> None:
    result = context.format_result
    # Plain format should not have JSON envelope structure
    assert '"command"' not in result, "Plain output should not contain JSON envelope"
    assert '"status"' not in result, "Plain output should not contain JSON envelope"
    assert '"exit_code"' not in result, "Plain output should not contain JSON envelope"
