import os
import stat
import subprocess
from pathlib import Path

import pytest

from imapsync_bulk import runner as runner_module
from imapsync_bulk.accounts import parse_accounts
from imapsync_bulk.exit_codes import describe, is_credential_failure
from imapsync_bulk.runner import ImapsyncMissing, build_argv, run_account

ACCOUNT_LINE = "srv1;alice;secret1;srv2;bob;secret2;;"


@pytest.fixture
def account():
    return parse_accounts(ACCOUNT_LINE)[0]


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


def fake_runner(returncode=0, capture=None):
    def run(argv, **kwargs):
        if capture is not None:
            capture.append(list(argv))
            # Les passfiles sont supprimes apres l'appel : on lit leur contenu
            # pendant que le "processus" tourne.
            capture.append(
                {
                    "passfiles": {
                        argv[i + 1]: Path(argv[i + 1]).read_text().strip()
                        for i, a in enumerate(argv)
                        if a in ("--passfile1", "--passfile2")
                    },
                    "modes": {
                        argv[i + 1]: stat.S_IMODE(os.stat(argv[i + 1]).st_mode)
                        for i, a in enumerate(argv)
                        if a in ("--passfile1", "--passfile2")
                    },
                }
            )
        return subprocess.CompletedProcess(argv, returncode, "", "")

    return run


def test_build_argv_follows_the_documented_option_names(account, tmp_path):
    argv = build_argv(
        account,
        tmp_path / "p1",
        tmp_path / "p2",
        dry_run=False,
        logdir=tmp_path / "logs",
    )
    assert argv[0] == "imapsync"
    for option in (
        "--host1",
        "--user1",
        "--passfile1",
        "--host2",
        "--user2",
        "--passfile2",
        "--logdir",
        "--logfile",
    ):
        assert option in argv
    assert argv[argv.index("--host1") + 1] == "srv1"
    assert argv[argv.index("--user2") + 1] == "bob"


def test_build_argv_omits_dry_by_default(account, tmp_path):
    argv = build_argv(
        account, tmp_path / "p1", tmp_path / "p2", dry_run=False, logdir=tmp_path
    )
    assert "--dry" not in argv


def test_build_argv_adds_dry_when_asked(account, tmp_path):
    argv = build_argv(
        account, tmp_path / "p1", tmp_path / "p2", dry_run=True, logdir=tmp_path
    )
    assert "--dry" in argv


def test_build_argv_never_contains_a_password(account, tmp_path):
    """La doc imapsync : --password1 expose le secret a `ps auxwwww`."""
    argv = build_argv(
        account, tmp_path / "p1", tmp_path / "p2", dry_run=False, logdir=tmp_path
    )
    assert "--password1" not in argv
    assert "--password2" not in argv
    assert "secret1" not in argv
    assert "secret2" not in argv


def test_extra_column_then_cli_args_are_appended(tmp_path):
    account = parse_accounts("h1;u1;p1;h2;u2;p2;--justfolders;")[0]
    argv = build_argv(
        account,
        tmp_path / "p1",
        tmp_path / "p2",
        dry_run=False,
        logdir=tmp_path,
        extra_args=["--automap"],
    )
    assert argv.index("--justfolders") < argv.index("--automap")


def test_run_account_writes_passfiles_in_0600(account, tmp_path, imapsync_present):
    capture: list = []
    run_account(
        account,
        dry_run=True,
        logdir=tmp_path / "logs",
        runner=fake_runner(capture=capture),
    )
    details = capture[1]
    assert set(details["passfiles"].values()) == {"secret1", "secret2"}
    assert set(details["modes"].values()) == {0o600}


def test_run_account_removes_passfiles_afterwards(account, tmp_path, imapsync_present):
    capture: list = []
    run_account(
        account,
        dry_run=True,
        logdir=tmp_path / "logs",
        runner=fake_runner(capture=capture),
    )
    argv = capture[0]
    for option in ("--passfile1", "--passfile2"):
        path = Path(argv[argv.index(option) + 1])
        assert not path.exists(), "un fichier de mot de passe a survecu"


def test_run_account_cleans_up_even_when_imapsync_crashes(
    account, tmp_path, imapsync_present
):
    created: list[Path] = []

    def exploding_runner(argv, **kwargs):
        for option in ("--passfile1", "--passfile2"):
            created.append(Path(argv[argv.index(option) + 1]))
        raise OSError("boom")

    with pytest.raises(OSError):
        run_account(
            account, dry_run=True, logdir=tmp_path / "logs", runner=exploding_runner
        )
    assert created
    assert not any(p.exists() for p in created)


def test_run_account_reports_the_exit_code(account, tmp_path, imapsync_present):
    result = run_account(
        account, dry_run=True, logdir=tmp_path / "logs", runner=fake_runner(16)
    )
    assert result.returncode == 16
    assert result.ok is False
    assert result.message == "echec d'authentification"


def test_run_account_requires_imapsync(account, tmp_path, monkeypatch):
    monkeypatch.setattr(runner_module, "find_imapsync", lambda: None)
    with pytest.raises(ImapsyncMissing):
        run_account(account, dry_run=True, logdir=tmp_path)


def test_logfile_is_per_account(tmp_path, imapsync_present):
    accounts = parse_accounts("h1;u1;p1;h2;u2;p2;;\nh1;other;p1;h2;u2;p2;;")
    logs = {
        run_account(
            a, dry_run=True, logdir=tmp_path / "logs", runner=fake_runner()
        ).logfile
        for a in accounts
    }
    assert len(logs) == 2


@pytest.mark.parametrize(
    "code,expected",
    [
        (0, "succes"),
        (16, "echec d'authentification"),
        (111, "termine avec des erreurs"),
        (161, "echec d'authentification pour user1"),
        (102, "echec de connexion sur host2"),
    ],
)
def test_exit_codes_are_translated(code, expected):
    assert describe(code) == expected


def test_credential_failures_are_flagged():
    assert is_credential_failure(16)
    assert is_credential_failure(161)
    assert is_credential_failure(162)
    assert not is_credential_failure(10)
