import io
import json

from interactive.interactive_config import load_interactive_profile
from interactive.interactive_keyboard import interpret_key, map_digit_to_value
from interactive.interactive_main import build_session_from_args, main, parse_args
from interactive.interactive_profiles import load_builtin_profile
from interactive.interactive_renderer import render_event_lines
from interactive.interactive_session import InteractiveSession


def test_interactive_cli_defaults_to_single_scalar_profile():
    parsed_args = parse_args(["--lang", "python", "--interactive"])
    session = build_session_from_args(parsed_args)

    assert session.profile.name == "single_scalar"
    assert session.normalize_inputs == "normalized"
    assert session.output_mode == "compact"
    assert session.trace_enabled is False


def test_interactive_config_replaces_profile_scaffold(tmp_path):
    config_path = tmp_path / "interactive_config.json"
    config_path.write_text(
        json.dumps(
            {
                "name": "dual_scalar_probe",
                "interactive": {
                    "output": "verbose",
                    "trace": False,
                    "normalize_inputs": "raw",
                },
                "input_space": {
                    "rank": 1,
                    "ports": [{"name": "a"}, {"name": "b"}],
                },
                "network": {
                    "regions": [
                        {
                            "name": "main",
                            "layers": [
                                {
                                    "excitatory_count": 3,
                                    "inhibitory_count": 1,
                                    "modulatory_count": 0,
                                }
                            ],
                        }
                    ]
                },
            }
        ),
        encoding="utf-8",
    )

    profile = load_interactive_profile("single_scalar", str(config_path))

    assert profile.name == "dual_scalar_probe"
    assert profile.input_ports == ["a", "b"]
    assert profile.default_normalize_inputs == "raw"
    assert profile.default_output == "verbose"
    assert len(profile.regions) == 1
    assert len(profile.regions[0].layers) == 1
    assert profile.regions[0].layers[0].excitatory_count == 3


def test_numeric_mapping_supports_normalized_and_raw_modes():
    assert map_digit_to_value(4, "normalized") == 0.4
    assert map_digit_to_value(4, "raw") == 4.0


def test_second_port_fallback_mapping_is_available_for_dual_input():
    command = interpret_key("Q", ["a", "b"])

    assert command.action == "input_tick"
    assert command.updated_port == "b"
    assert command.digit_value == 1


def test_dual_input_profile_retains_other_port_state_between_ticks():
    session = InteractiveSession(load_builtin_profile("dual_scalar"))

    first_event = session.handle_key("2")
    second_event = session.handle_key("Q")
    third_event = session.handle_key("5")

    assert first_event.snapshot is not None
    assert second_event.snapshot is not None
    assert third_event.snapshot is not None

    assert first_event.snapshot.input_values["a"] == 0.2
    assert first_event.snapshot.input_values["b"] == 0.0
    assert second_event.snapshot.input_values["a"] == 0.2
    assert second_event.snapshot.input_values["b"] == 0.1
    assert third_event.snapshot.input_values["a"] == 0.5
    assert third_event.snapshot.input_values["b"] == 0.1


def test_reset_rebuilds_original_scaffold_after_growth():
    session = InteractiveSession(load_builtin_profile("growth_probe"))
    initial_snapshot = session.build_summary_snapshot()

    session.handle_key("1")
    session.handle_key("9")
    grown_snapshot = session.build_summary_snapshot()
    session.handle_key("r")
    reset_snapshot = session.build_summary_snapshot()

    assert grown_snapshot.neuron_count >= initial_snapshot.neuron_count
    assert session.tick_number == 0
    assert reset_snapshot.neuron_count == initial_snapshot.neuron_count
    assert reset_snapshot.layer_count == initial_snapshot.layer_count
    assert reset_snapshot.total_slots == initial_snapshot.total_slots


def test_compact_output_contains_tick_profile_and_updated_port():
    session = InteractiveSession(load_builtin_profile("single_scalar"))
    event = session.handle_key("4")

    rendered_lines = render_event_lines(session, event)

    assert rendered_lines
    assert "tick=1" in rendered_lines[-1]
    assert "profile=single_scalar" in rendered_lines[-1]
    assert "updated_port=a" in rendered_lines[-1]


def test_jsonl_output_emits_parseable_tick_summary():
    session = InteractiveSession(load_builtin_profile("single_scalar"), output_mode="jsonl")
    event = session.handle_key("4")

    rendered_lines = render_event_lines(session, event)
    payload = json.loads(rendered_lines[-1])

    assert payload["event"] == "interactive.tick"
    assert payload["profile"] == "single_scalar"
    assert payload["updated_port"] == "a"
    assert payload["input_a"] == 0.4


def test_interactive_snapshot_surfaces_scalar_focus_anchor_summary():
    session = InteractiveSession(load_builtin_profile("single_scalar"), output_mode="compact")

    event = session.handle_key("4")

    assert event.snapshot is not None
    assert event.snapshot.focus_policy == "scalar_anchor"
    assert event.snapshot.anchor_value == 0.4
    rendered_lines = render_event_lines(session, event)
    assert "policy=scalar_anchor" in rendered_lines[-1]
    assert "anchor=0.400" in rendered_lines[-1]


def test_interactive_main_smoke_quits_cleanly():
    output_buffer = io.StringIO()
    key_values = iter(["q"])

    exit_code = main(
        ["--lang", "python", "--interactive"],
        key_reader=lambda: next(key_values),
        output_writer=output_buffer,
    )

    output_text = output_buffer.getvalue()
    assert exit_code == 0
    assert "GrowNet interactive mode" in output_text
    assert "Quitting interactive session." in output_text
