import subprocess

import pytest

from imapsync_bulk import runner as runner_module
from imapsync_bulk.cli import build_parser, main, split_passthrough

TWO_ACCOUNTS = "srv1;alice;s1;srv2;bob;s2;;\nsrv1;carol;s3;srv2;dave;s4;;\n"


@pytest.fixture
def imapsync_present(monkeypatch):
    monkeypatch.setattr(runner_module, "find_imapsync", lambda: "/usr/bin/imapsync")
    monkeypatch.setattr("imapsync_bulk.cli.find_imapsync", lambda: "/usr/bin/imapsync")


@pytest.fixture
def calls(monkeypatch):
    recorded: list[list[str]] = []

    def spy(argv, **kwargs):
        recorded.append(list(argv))
        return subprocess.CompletedProcess(argv, 0, "", "")

    monkeypatch.setattr(subprocess, "run", spy)
    return recorded


def _file(tmp_path, content=TWO_ACCOUNTS):
    path = tmp_path / "comptes.txt"
    path.write_text(content)
    return path


def test_missing_file_exits_2(tmp_path, imapsync_present):
    assert main([str(tmp_path / "absent.txt")]) == 2


def test_invalid_file_exits_2(tmp_path, imapsync_present):
    assert main([str(_file(tmp_path, "h1;u1;p1\n"))]) == 2


def test_missing_imapsync_exits_2(tmp_path, monkeypatch):
    monkeypatch.setattr("imapsync_bulk.cli.find_imapsync", lambda: None)
    assert main([str(_file(tmp_path))]) == 2


def test_check_validates_without_running(tmp_path, imapsync_present, calls, capsys):
    assert main([str(_file(tmp_path)), "--check"]) == 0
    assert calls == []
    out = capsys.readouterr().out
    assert "2 compte(s)" in out
    assert "alice" in out
    assert "srv1" in out


def test_runs_every_account(tmp_path, imapsync_present, calls):
    assert main([str(_file(tmp_path)), "--logdir", str(tmp_path / "logs")]) == 0
    assert len(calls) == 2


def test_real_run_has_no_dry_flag(tmp_path, imapsync_present, calls):
    main([str(_file(tmp_path)), "--logdir", str(tmp_path / "logs")])
    assert all("--dry" not in argv for argv in calls)


def test_dry_run_passes_dry_to_imapsync(tmp_path, imapsync_present, calls):
    main([str(_file(tmp_path)), "--dry-run", "--logdir", str(tmp_path / "logs")])
    assert all("--dry" in argv for argv in calls)


def test_passthrough_args_reach_imapsync(tmp_path, imapsync_present, calls):
    main(
        [
            str(_file(tmp_path)),
            "--logdir",
            str(tmp_path / "logs"),
            "--",
            "--justfolders",
            "--automap",
        ]
    )
    assert all("--justfolders" in argv and "--automap" in argv for argv in calls)


def test_no_password_ever_reaches_the_command_line(tmp_path, imapsync_present, calls):
    main([str(_file(tmp_path)), "--logdir", str(tmp_path / "logs")])
    flat = " ".join(a for argv in calls for a in argv)
    for secret in ("s1", "s2", "s3", "s4"):
        assert f" {secret} " not in f" {flat} "
    assert "--password1" not in flat
    assert "--password2" not in flat


def test_failure_returns_1_and_continues(tmp_path, imapsync_present, monkeypatch):
    seen: list[list[str]] = []

    def failing(argv, **kwargs):
        seen.append(list(argv))
        return subprocess.CompletedProcess(argv, 16, "", "")

    monkeypatch.setattr(subprocess, "run", failing)
    code = main([str(_file(tmp_path)), "--logdir", str(tmp_path / "logs")])
    assert code == 1
    assert len(seen) == 2  # l'echec du premier n'arrete pas la serie


def test_stop_on_error_halts_the_series(tmp_path, imapsync_present, monkeypatch):
    seen: list[list[str]] = []

    def failing(argv, **kwargs):
        seen.append(list(argv))
        return subprocess.CompletedProcess(argv, 10, "", "")

    monkeypatch.setattr(subprocess, "run", failing)
    code = main(
        [str(_file(tmp_path)), "--stop-on-error", "--logdir", str(tmp_path / "logs")]
    )
    assert code == 1
    assert len(seen) == 1


def test_credential_failures_are_called_out(tmp_path, imapsync_present, monkeypatch, capsys):
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda argv, **kw: subprocess.CompletedProcess(argv, 161, "", ""),
    )
    main([str(_file(tmp_path)), "--logdir", str(tmp_path / "logs")])
    assert "identifiants refuses" in capsys.readouterr().out.replace("\n", " ")


def test_parser_defaults_to_real_migration():
    args = build_parser().parse_args(["comptes.txt"])
    assert args.dry_run is False


def test_options_after_the_filename_are_still_ours(tmp_path, imapsync_present, calls):
    """Regression : nos options placees apres le fichier doivent etre lues.

    Avec nargs=REMAINDER, `comptes.txt --dry-run` etait avale comme argument
    imapsync et la migration partait pour de vrai.
    """
    main([str(_file(tmp_path)), "--dry-run", "--logdir", str(tmp_path / "logs")])
    assert calls, "aucune commande construite"
    assert all("--dry" in argv for argv in calls)


def test_split_passthrough_separates_on_the_first_double_dash():
    own, extra = split_passthrough(["f.txt", "--dry-run", "--", "--justfolders"])
    assert own == ["f.txt", "--dry-run"]
    assert extra == ["--justfolders"]


def test_split_passthrough_without_double_dash():
    own, extra = split_passthrough(["f.txt", "--dry-run"])
    assert own == ["f.txt", "--dry-run"]
    assert extra == []


def test_unknown_option_is_rejected_not_forwarded(tmp_path, imapsync_present):
    with pytest.raises(SystemExit):
        main([str(_file(tmp_path)), "--optionquinexistepas"])


def test_full_sync_adds_automap(tmp_path, imapsync_present, calls):
    main([str(_file(tmp_path)), "--full-sync", "--logdir", str(tmp_path / "logs")])
    assert all("--automap" in argv for argv in calls)


def test_full_sync_never_adds_a_delete_option(tmp_path, imapsync_present, calls):
    """Le preset est additif : aucune suppression cote destination."""
    main([str(_file(tmp_path)), "--full-sync", "--logdir", str(tmp_path / "logs")])
    flat = " ".join(a for argv in calls for a in argv)
    assert "--delete2" not in flat
    assert "--delete2folders" not in flat


def test_full_sync_is_not_duplicated_when_already_in_the_file(
    tmp_path, imapsync_present, calls
):
    content = "srv1;alice;s1;srv2;bob;s2;--automap;\n"
    main([str(_file(tmp_path, content)), "--full-sync", "--logdir", str(tmp_path / "l")])
    assert calls[0].count("--automap") == 1


def test_repeatable_options_are_not_deduplicated(tmp_path, imapsync_present, calls):
    """--folder est repetable dans imapsync : ne jamais le filtrer."""
    content = "srv1;alice;s1;srv2;bob;s2;--folder INBOX;\n"
    main(
        [
            str(_file(tmp_path, content)),
            "--logdir",
            str(tmp_path / "l"),
            "--",
            "--folder",
            "Archives",
        ]
    )
    assert calls[0].count("--folder") == 2


CSV_FILE = (
    "host1,email1,password1,host2,email2,password2\n"
    "srv1,alice@x.fr,s1,srv2,alice@x.fr,s2\n"
    "srv1,bob@x.fr,s3,srv2,bob@x.fr,s4\n"
    "srv1,carol@x.fr,s5,srv2,carol@x.fr,s6\n"
)


def _csv(tmp_path):
    path = tmp_path / "comptes.csv"
    path.write_text(CSV_FILE)
    return path


def test_csv_is_read_directly(tmp_path, imapsync_present, calls):
    assert main([str(_csv(tmp_path)), "--logdir", str(tmp_path / "l")]) == 0
    assert len(calls) == 3


def test_line_selects_a_single_account(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--line", "1", "--dry-run", "--logdir", str(tmp_path / "l")])
    assert len(calls) == 1
    assert "alice@x.fr" in calls[0]


def test_line_two_selects_the_second(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--line", "2", "--logdir", str(tmp_path / "l")])
    assert len(calls) == 1
    assert "bob@x.fr" in calls[0]


def test_line_is_repeatable(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--line", "1", "--line", "3", "--logdir", str(tmp_path / "l")])
    assert len(calls) == 2


def test_line_accepts_a_range(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--line", "2-3", "--logdir", str(tmp_path / "l")])
    assert len(calls) == 2


def test_line_out_of_range_exits_2(tmp_path, imapsync_present, calls):
    assert main([str(_csv(tmp_path)), "--line", "12"]) == 2
    assert calls == []


def test_line_zero_exits_2(tmp_path, imapsync_present):
    assert main([str(_csv(tmp_path)), "--line", "0"]) == 2


def test_line_non_numeric_exits_2(tmp_path, imapsync_present):
    assert main([str(_csv(tmp_path)), "--line", "abc"]) == 2


def test_check_shows_stable_numbers_under_selection(tmp_path, imapsync_present, capsys):
    main([str(_csv(tmp_path)), "--line", "3", "--check"])
    out = capsys.readouterr().out
    assert "n°   3" in out.replace("  ", "  ")
    assert "carol@x.fr" in out


def test_csv_passwords_never_reach_the_command_line(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--logdir", str(tmp_path / "l")])
    flat = " ".join(a for argv in calls for a in argv)
    assert "--password1" not in flat
    for secret in ("s1", "s2", "s3", "s4", "s5", "s6"):
        assert f" {secret} " not in f" {flat} "


def test_just_login_passes_justlogin(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--just-login", "--logdir", str(tmp_path / "l")])
    assert all("--justlogin" in argv for argv in calls)


def test_just_login_does_not_imply_dry(tmp_path, imapsync_present, calls):
    """--justlogin sort avant tout transfert : --dry serait redondant."""
    main([str(_csv(tmp_path)), "--just-login", "--logdir", str(tmp_path / "l")])
    assert all("--dry" not in argv for argv in calls)


def test_label_does_not_glue_two_at_signs(tmp_path):
    from imapsync_bulk.accounts import parse_csv_accounts

    (account,) = parse_csv_accounts(
        "host1,email1,password1,host2,email2,password2\n"
        "mail.advmfr.net,bcc@advmfr.net,p1,arome.o2switch.net,bcc@advmfr.net,p2\n"
    )
    assert "advmfr.net@mail" not in account.label
    assert "bcc@advmfr.net" in account.label
    assert "mail.advmfr.net" in account.label
    assert "arome.o2switch.net" in account.label


def test_catch_up_limits_to_inbox_and_recent(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--catch-up", "--logdir", str(tmp_path / "l")])
    argv = calls[0]
    assert argv[argv.index("--folderrec") + 1] == "INBOX"
    assert argv[argv.index("--maxage") + 1] == "1"


def test_catch_up_days_is_configurable(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--catch-up", "--catch-up-days", "3",
          "--logdir", str(tmp_path / "l")])
    argv = calls[0]
    assert argv[argv.index("--maxage") + 1] == "3"


def test_catch_up_is_not_destructive_by_default(tmp_path, imapsync_present, calls):
    """La FAQ propose --delete1, mais il detruit cote source : jamais implicite."""
    main([str(_csv(tmp_path)), "--catch-up", "--logdir", str(tmp_path / "l")])
    flat = " ".join(a for argv in calls for a in argv)
    assert "--delete1" not in flat
    assert "--delete2" not in flat


def test_use_cache_is_opt_in(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--logdir", str(tmp_path / "l")])
    assert all("--usecache" not in argv for argv in calls)
    calls.clear()
    main([str(_csv(tmp_path)), "--use-cache", "--logdir", str(tmp_path / "l")])
    assert all("--usecache" in argv for argv in calls)


def test_catch_up_combines_with_line(tmp_path, imapsync_present, calls):
    main([str(_csv(tmp_path)), "--catch-up", "--line", "2",
          "--logdir", str(tmp_path / "l")])
    assert len(calls) == 1
    assert "bob@x.fr" in calls[0]
    assert "--maxage" in calls[0]


# --- garde-fou : aucune suppression, jamais -------------------------------


def test_no_flag_combination_can_produce_a_delete(tmp_path, imapsync_present, calls):
    """Aucune combinaison d'options ne doit produire une option de suppression."""
    main([
        str(_csv(tmp_path)), "--catch-up", "--full-sync", "--use-cache",
        "--catch-up-days", "7", "--logdir", str(tmp_path / "l"),
    ])
    flat = " ".join(a for argv in calls for a in argv)
    for flag in ("--delete", "--delete1", "--delete2", "--delete2folders", "--expunge1"):
        assert flag not in flat


def test_delete_via_passthrough_is_refused(tmp_path, imapsync_present, calls):
    code = main([str(_csv(tmp_path)), "--logdir", str(tmp_path / "l"),
                 "--", "--delete2"])
    assert code == 2
    assert calls == [], "aucune commande ne doit partir"


def test_delete1_via_passthrough_is_refused(tmp_path, imapsync_present, calls):
    assert main([str(_csv(tmp_path)), "--", "--delete1"]) == 2
    assert calls == []


def test_delete_in_the_accounts_file_is_refused(tmp_path, imapsync_present, calls):
    content = "srv1;alice;s1;srv2;bob;s2;--delete2folders;\n"
    assert main([str(_file(tmp_path, content))]) == 2
    assert calls == []


def test_attached_form_is_also_refused(tmp_path, imapsync_present, calls):
    assert main([str(_csv(tmp_path)), "--", "--delete2=1"]) == 2
    assert calls == []


def test_harmless_passthrough_still_works(tmp_path, imapsync_present, calls):
    """Le garde-fou ne doit pas bloquer les options legitimes."""
    assert main([str(_csv(tmp_path)), "--logdir", str(tmp_path / "l"),
                 "--", "--justfolders"]) == 0
    assert all("--justfolders" in argv for argv in calls)


def test_catch_up_uses_folderrec_not_folder(tmp_path, imapsync_present, calls):
    """--folder INBOX raterait les sous-dossiers d'un serveur a namespace INBOX."""
    main([str(_csv(tmp_path)), "--catch-up", "--logdir", str(tmp_path / "l")])
    argv = calls[0]
    assert "--folderrec" in argv
    assert argv[argv.index("--folderrec") + 1] == "INBOX"
    assert "--folder" not in argv


def test_each_account_gets_its_own_pidfile(tmp_path, imapsync_present, calls):
    """Le pidfile par defaut est partage : deux instances se marcheraient dessus."""
    main([str(_csv(tmp_path)), "--logdir", str(tmp_path / "l")])
    pidfiles = [argv[argv.index("--pidfile") + 1] for argv in calls]
    assert len(pidfiles) == 3
    assert len(set(pidfiles)) == 3, "les pidfiles doivent etre distincts"


def test_dates_and_flags_are_left_to_imapsync_defaults(tmp_path, imapsync_present, calls):
    """--syncinternaldates et --resyncflags sont ON par defaut : ne pas y toucher."""
    main([str(_csv(tmp_path)), "--full-sync", "--catch-up",
          "--logdir", str(tmp_path / "l")])
    flat = " ".join(a for argv in calls for a in argv)
    for flag in ("--nosyncinternaldates", "--noresyncflags", "--nosyncflags"):
        assert flag not in flat


def test_plan_lists_every_step_without_running(tmp_path, imapsync_present, calls, capsys):
    assert main([str(_csv(tmp_path)), "--plan"]) == 0
    assert calls == [], "--plan ne doit rien lancer"
    out = capsys.readouterr().out
    assert "Plan de migration" in out
    for number in range(1, 8):
        assert f"{number}." in out


def test_plan_shows_the_steps_outside_the_tool(tmp_path, imapsync_present, capsys):
    """L'interet du plan : montrer ce que l'outil ne fait PAS."""
    main([str(_csv(tmp_path)), "--plan"])
    out = capsys.readouterr().out.lower()
    assert "hors outil" in out
    assert "ttl" in out
    assert "mx" in out


def test_plan_names_the_real_commands(tmp_path, imapsync_present, capsys):
    main([str(_csv(tmp_path)), "--plan"])
    out = capsys.readouterr().out
    assert "--just-login" in out
    assert "--catch-up" in out
    assert "--dry-run" in out
