from __future__ import annotations

import argparse

import pytest

from beach.cli import (
    analyze_coulomb_mobility,
    animate_fortran_history,
    config,
    estimate_fortran_workload,
    inspect_fortran_output,
    legacy,
    model_close_pack,
    preset,
    plot_coulomb_force_matrix,
    plot_fortran_potential_slices,
    plot_performance_profile,
)
from beach.cli.main import build_parser as build_beachx_parser
from beach.cli.main import main as beachx_main


CLI_CASES = [
    (
        "inspect",
        inspect_fortran_output.build_parser,
        legacy.inspect_main,
        ["no_such_dir"],
        'Fortran output files are missing under "no_such_dir". '
        "Expected at least summary.txt and charges.csv.",
        "WARNING: `beach-inspect` is deprecated; use `beachx inspect` instead.",
    ),
    (
        "animate",
        animate_fortran_history.build_parser,
        legacy.animate_main,
        ["no_such_dir"],
        'Fortran output files are missing under "no_such_dir". '
        "Expected at least summary.txt and charges.csv.",
        "WARNING: `beach-animate-history` is deprecated; use `beachx animate` instead.",
    ),
    (
        "coulomb",
        plot_coulomb_force_matrix.build_parser,
        legacy.coulomb_main,
        ["no_such_dir"],
        'Fortran output files are missing under "no_such_dir". '
        "Expected at least summary.txt and charges.csv.",
        "WARNING: `beach-plot-coulomb-force-matrix` is deprecated; use `beachx coulomb` instead.",
    ),
    (
        "slices",
        plot_fortran_potential_slices.build_parser,
        legacy.slices_main,
        ["no_such_dir"],
        'Fortran output files are missing under "no_such_dir". '
        "Expected at least summary.txt and charges.csv.",
        "WARNING: `beach-plot-potential-slices` is deprecated; use `beachx slices` instead.",
    ),
    (
        "workload",
        estimate_fortran_workload.build_parser,
        legacy.workload_main,
        ["no_such_file.toml"],
        "config file not found: no_such_file.toml",
        "WARNING: `beach-estimate-workload` is deprecated; use `beachx workload` instead.",
    ),
    (
        "profile",
        plot_performance_profile.build_parser,
        legacy.profile_main,
        ["no_such_dir"],
        'Performance profile file is missing under "no_such_dir/performance_profile.csv". '
        "Expected performance_profile.csv.",
        "WARNING: `beach-plot-performance-profile` is deprecated; use `beachx profile` instead.",
    ),
]


def _get_subparser(command: str) -> argparse.ArgumentParser:
    parser = build_beachx_parser()
    subparsers = next(
        action
        for action in parser._actions
        if isinstance(action, argparse._SubParsersAction)
    )
    return subparsers.choices[command]


def _get_nested_subparser(*commands: str) -> argparse.ArgumentParser:
    parser = build_beachx_parser()
    current_parser = parser
    for command in commands:
        subparsers = next(
            action
            for action in current_parser._actions
            if isinstance(action, argparse._SubParsersAction)
        )
        current_parser = subparsers.choices[command]
    return current_parser


def _parser_signature(parser: argparse.ArgumentParser) -> list[tuple[object, ...]]:
    signature: list[tuple[object, ...]] = []
    for action in parser._actions:
        if isinstance(action, argparse._HelpAction):
            continue
        choices = action.choices
        if choices is None:
            normalized_choices: object = None
        elif isinstance(choices, dict):
            normalized_choices = tuple(sorted(choices))
        else:
            normalized_choices = tuple(choices)
        signature.append(
            (
                type(action).__name__,
                tuple(action.option_strings),
                action.dest,
                action.nargs,
                action.required,
                normalized_choices,
            )
        )
    return signature


def test_beachx_help_lists_all_subcommands(capsys: pytest.CaptureFixture[str]) -> None:
    with pytest.raises(SystemExit) as excinfo:
        beachx_main(["--help"])

    assert excinfo.value.code == 0
    captured = capsys.readouterr()
    assert "inspect" in captured.out
    assert "animate" in captured.out
    assert "coulomb" in captured.out
    assert "mobility" in captured.out
    assert "slices" in captured.out
    assert "workload" in captured.out
    assert "profile" in captured.out
    assert "config" in captured.out
    assert "preset" in captured.out
    assert "model" in captured.out


def test_beachx_model_help_lists_available_models(
    capsys: pytest.CaptureFixture[str],
) -> None:
    with pytest.raises(SystemExit) as excinfo:
        beachx_main(["model", "--help"])

    assert excinfo.value.code == 0
    captured = capsys.readouterr()
    assert "close-pack" in captured.out


def test_beachx_config_help_lists_available_subcommands(
    capsys: pytest.CaptureFixture[str],
) -> None:
    with pytest.raises(SystemExit) as excinfo:
        beachx_main(["config", "--help"])

    assert excinfo.value.code == 0
    captured = capsys.readouterr()
    assert "init" in captured.out
    assert "render" in captured.out
    assert "validate" in captured.out
    assert "diff" in captured.out
    assert "save" in captured.out
    assert "list-saved" in captured.out


def test_beachx_preset_help_lists_available_subcommands(
    capsys: pytest.CaptureFixture[str],
) -> None:
    with pytest.raises(SystemExit) as excinfo:
        beachx_main(["preset", "--help"])

    assert excinfo.value.code == 0
    captured = capsys.readouterr()
    assert "new" in captured.out
    assert "save" in captured.out
    assert "list" in captured.out
    assert "edit" in captured.out
    assert "show" in captured.out
    assert "path" in captured.out
    assert "validate" in captured.out


def test_beachx_mobility_subparser_matches_parser_shape() -> None:
    assert _parser_signature(_get_subparser("mobility")) == _parser_signature(
        analyze_coulomb_mobility.build_parser()
    )


def test_beachx_model_close_pack_subparser_matches_parser_shape() -> None:
    assert _parser_signature(
        _get_nested_subparser("model", "close-pack")
    ) == _parser_signature(model_close_pack.build_parser())


def test_beachx_config_render_subparser_matches_parser_shape() -> None:
    config_parser = config.build_parser()
    config_subparsers = next(
        action
        for action in config_parser._actions
        if isinstance(action, argparse._SubParsersAction)
    )
    assert _parser_signature(
        _get_nested_subparser("config", "render")
    ) == _parser_signature(config_subparsers.choices["render"])


def test_beachx_preset_new_subparser_matches_parser_shape() -> None:
    preset_parser = preset.build_parser()
    preset_subparsers = next(
        action
        for action in preset_parser._actions
        if isinstance(action, argparse._SubParsersAction)
    )
    assert _parser_signature(
        _get_nested_subparser("preset", "new")
    ) == _parser_signature(preset_subparsers.choices["new"])


def test_beachx_preset_save_subparser_matches_parser_shape() -> None:
    preset_parser = preset.build_parser()
    preset_subparsers = next(
        action
        for action in preset_parser._actions
        if isinstance(action, argparse._SubParsersAction)
    )
    assert _parser_signature(
        _get_nested_subparser("preset", "save")
    ) == _parser_signature(preset_subparsers.choices["save"])


def test_beachx_model_close_pack_missing_base_config_exits_with_friendly_message(
    tmp_path,
    monkeypatch,
) -> None:
    monkeypatch.chdir(tmp_path)
    with pytest.raises(SystemExit) as excinfo:
        beachx_main(["model", "close-pack", "--layers", "4", "--radius", "0.2", "--cells-x", "2"])

    assert str(excinfo.value) == "base config file not found: beach.toml"


def test_beachx_mobility_missing_output_dir_exits_with_friendly_message() -> None:
    with pytest.raises(SystemExit) as excinfo:
        beachx_main(["mobility", "no_such_dir"])

    assert (
        str(excinfo.value)
        == 'Fortran output files are missing under "no_such_dir". Expected at least summary.txt and charges.csv.'
    )


@pytest.mark.parametrize(
    ("command", "build_legacy_parser", "legacy_main", "_argv", "_message", "_warning"),
    CLI_CASES,
)
def test_beachx_subparser_matches_legacy_parser_shape(
    command: str,
    build_legacy_parser,
    legacy_main,
    _argv: list[str],
    _message: str,
    _warning: str,
) -> None:
    del legacy_main, _argv, _message, _warning
    assert _parser_signature(_get_subparser(command)) == _parser_signature(
        build_legacy_parser()
    )


@pytest.mark.parametrize(
    ("command", "_build_legacy_parser", "legacy_main", "argv", "message", "warning"),
    CLI_CASES,
)
def test_legacy_alias_warns_and_matches_beachx_errors(
    capsys: pytest.CaptureFixture[str],
    command: str,
    _build_legacy_parser,
    legacy_main,
    argv: list[str],
    message: str,
    warning: str,
) -> None:
    del _build_legacy_parser

    with pytest.raises(SystemExit) as legacy_exc:
        legacy_main(argv)
    legacy_streams = capsys.readouterr()

    with pytest.raises(SystemExit) as beachx_exc:
        beachx_main([command, *argv])
    beachx_streams = capsys.readouterr()

    assert str(legacy_exc.value) == message
    assert str(beachx_exc.value) == message
    assert legacy_streams.err.strip() == warning
    assert beachx_streams.err == ""
