# https://docs.pytest.org/en/7.1.x/example/parametrize.html#parametrizing-conditional-raising
import shutil
from contextlib import nullcontext as does_not_raise
from pathlib import Path

import pytest
from click.testing import CliRunner

from xorq.catalog.catalog import (
    BuildTgz,
    Catalog,
    CatalogAddition,
)
from xorq.catalog.cli import cli
from xorq.catalog.tar_utils import (
    REQUIRED_TGZ_NAMES,
    extract_build_tgz_context,
    write_tgz,
)
from xorq.catalog.tests.conftest import (
    compare_repo_and_catalog,
    make_build_tgz,
)


@pytest.fixture
def runner():
    yield CliRunner()


# --- init command ---


def test_init_command(runner, tmpdir):
    new_path = str(Path(tmpdir).joinpath("init-catalog"))
    result = runner.invoke(cli, ["--path", new_path, "init"])
    assert result.exit_code == 0, result.output
    assert "Initialized catalog at" in result.output
    assert Path(new_path).exists()


def test_init_command_already_exists(runner, catalog_path):
    assert Path(catalog_path).exists()
    result = runner.invoke(cli, ["--path", catalog_path, "init"])
    assert result.exit_code != 0
    assert "already exists" in result.output
    assert catalog_path in result.output


def test_init_by_name(runner, tmpdir, monkeypatch):
    monkeypatch.setattr(Catalog, "by_name_base_path", Path(tmpdir))
    result = runner.invoke(cli, ["--name", "my-catalog", "init"])
    assert result.exit_code == 0, result.output
    assert "Initialized catalog at" in result.output
    assert Path(tmpdir).joinpath("my-catalog").exists()


def test_init_by_name_already_exists(runner, tmpdir, monkeypatch):
    monkeypatch.setattr(Catalog, "by_name_base_path", Path(tmpdir))
    runner.invoke(cli, ["--name", "my-catalog", "init"])
    result = runner.invoke(cli, ["--name", "my-catalog", "init"])
    assert result.exit_code != 0


def test_init_default(runner, tmpdir, monkeypatch):
    monkeypatch.setattr(Catalog, "by_name_base_path", Path(tmpdir))
    result = runner.invoke(cli, ["init"])
    assert result.exit_code == 0, result.output
    assert "Initialized catalog at" in result.output
    assert Path(tmpdir).joinpath("default").exists()


def test_init_default_already_exists(runner, tmpdir, monkeypatch):
    monkeypatch.setattr(Catalog, "by_name_base_path", Path(tmpdir))
    runner.invoke(cli, ["init"])
    result = runner.invoke(cli, ["init"])
    assert result.exit_code != 0


def test_init_with_root_repo_and_name(runner, root_repo):
    result = runner.invoke(
        cli,
        ["--root-repo", root_repo.working_dir, "--name", "sub-catalog", "init"],
    )
    assert result.exit_code == 0, result.output
    assert "Initialized catalog at" in result.output


# --- add command ---


def test_add_command(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    result = runner.invoke(cli, ["--path", catalog_path, "add", path])
    assert result.exit_code == 0, result.output
    assert "Added" in result.output


def test_add_multiple(runner, catalog_path, data_dict):
    paths = [str(p) for p in data_dict.values()]
    result = runner.invoke(cli, ["--path", catalog_path, "add", *paths])
    assert result.exit_code == 0, result.output
    assert result.output.count("Added") == len(data_dict)


def test_add_with_aliases(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    result = runner.invoke(
        cli,
        [
            "--path",
            catalog_path,
            "add",
            path,
            "--alias",
            "alias-x",
            "--alias",
            "alias-y",
        ],
    )
    assert result.exit_code == 0, result.output
    catalog = Catalog(repo=Catalog.from_kwargs(path=catalog_path, init=False).repo)
    assert {ca.alias for ca in catalog.catalog_aliases} == {"alias-x", "alias-y"}


def test_add_duplicate(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    result = runner.invoke(cli, ["--path", catalog_path, "add", path])
    assert result.exit_code != 0


def test_add_from_directory(runner, catalog_path, tmpdir):
    tgz = make_build_tgz(tmpdir, "build-dir-test")
    with extract_build_tgz_context(tgz) as build_dir:
        result = runner.invoke(cli, ["--path", catalog_path, "add", str(build_dir)])
    assert result.exit_code == 0, result.output
    assert "Added" in result.output


def test_add_nonexistent_path(runner, catalog_path):
    result = runner.invoke(
        cli, ["--path", catalog_path, "add", "/nonexistent/file.tgz"]
    )
    assert result.exit_code != 0


@pytest.mark.parametrize(
    "sync,expectation",
    (
        (True, does_not_raise()),
        (False, pytest.raises(AssertionError)),
    ),
)
def test_add_sync(sync, expectation, runner, repo_cloned_bare, tmpdir):
    cloned = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("add-sync-test")
    )

    path = make_build_tgz(tmpdir, "to-add")
    result = runner.invoke(
        cli,
        [
            "--path",
            cloned.repo_path,
            "add",
            str(path),
            "--sync" if sync else "--no-sync",
        ],
    )
    assert result.exit_code == 0, result.output
    assert "Added" in result.output

    # check sync condition
    with expectation:
        compare_repo_and_catalog(repo_cloned_bare, cloned)


# --- remove command ---


def test_remove_command(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    result = runner.invoke(cli, ["--path", catalog_path, "remove", name])
    assert result.exit_code == 0, result.output
    assert "Removed" in result.output


def test_remove_nonexistent(runner, catalog_path):
    result = runner.invoke(cli, ["--path", catalog_path, "remove", "nonexistent"])
    assert result.exit_code != 0


def test_remove_multiple(runner, catalog_path, data_dict):
    paths = [str(p) for p in data_dict.values()]
    runner.invoke(cli, ["--path", catalog_path, "add", *paths])
    names = [
        Path(p).name.removesuffix("".join(Path(p).suffixes)) for p in data_dict.values()
    ]
    result = runner.invoke(cli, ["--path", catalog_path, "remove", *names])
    assert result.exit_code == 0, result.output
    assert result.output.count("Removed") == len(data_dict)


@pytest.mark.parametrize(
    "sync,expectation",
    (
        (True, does_not_raise()),
        (False, pytest.raises(AssertionError)),
    ),
)
def test_remove_sync(sync, expectation, runner, repo_cloned_bare, tmpdir, data_dict):
    cloned = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("remove-sync-test")
    )

    path = str(next(iter(data_dict.values())))
    result = runner.invoke(
        cli,
        [
            "--path",
            cloned.repo_path,
            "remove",
            Path(path).stem,
            "--sync" if sync else "--no-sync",
        ],
    )
    assert result.exit_code == 0, result.output
    assert "Removed" in result.output

    # check that sync condition
    with expectation:
        compare_repo_and_catalog(repo_cloned_bare, cloned)


# --- add-alias command ---


def test_add_alias_command(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    result = runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "my-alias"])
    assert result.exit_code == 0, result.output
    assert "my-alias" in result.output


def test_add_with_aliases_commit_message(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    result = runner.invoke(
        cli,
        ["--path", catalog_path, "add", path, "--alias", "v1", "--alias", "latest"],
    )
    assert result.exit_code == 0, result.output
    catalog = Catalog.from_kwargs(path=catalog_path, init=False)
    commit_message = catalog.repo.head.commit.message.strip()
    assert "v1" in commit_message
    assert "latest" in commit_message


def test_add_alias_unknown_entry(runner, catalog_path):
    result = runner.invoke(
        cli, ["--path", catalog_path, "add-alias", "nonexistent", "my-alias"]
    )
    assert result.exit_code != 0


def test_add_alias_overwrite(runner, catalog_path, data_dict):
    paths = [str(p) for p in data_dict.values()]
    runner.invoke(cli, ["--path", catalog_path, "add", *paths])
    names = [
        Path(p).name.removesuffix("".join(Path(p).suffixes)) for p in data_dict.values()
    ]
    runner.invoke(cli, ["--path", catalog_path, "add-alias", names[0], "shared"])
    result = runner.invoke(
        cli, ["--path", catalog_path, "add-alias", names[1], "shared"]
    )
    assert result.exit_code == 0, result.output
    assert "shared" in result.output


# --- remove-alias command ---


def test_remove_alias_command(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "to-remove"])
    result = runner.invoke(cli, ["--path", catalog_path, "remove-alias", "to-remove"])
    assert result.exit_code == 0, result.output
    assert "to-remove" in result.output


def test_remove_alias_multiple(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "alias-a"])
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "alias-b"])
    result = runner.invoke(
        cli, ["--path", catalog_path, "remove-alias", "alias-a", "alias-b"]
    )
    assert result.exit_code == 0, result.output
    assert result.output.count("Removed alias") == 2


def test_remove_alias_nonexistent(runner, catalog_path):
    result = runner.invoke(
        cli, ["--path", catalog_path, "remove-alias", "no-such-alias"]
    )
    assert result.exit_code != 0


@pytest.mark.parametrize(
    "sync,expectation",
    (
        (True, does_not_raise()),
        (False, pytest.raises(AssertionError)),
    ),
)
def test_add_alias_sync(sync, expectation, runner, repo_cloned_bare, tmpdir):
    cloned = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("add-alias-sync-test")
    )
    path = make_build_tgz(tmpdir, "to-alias")
    runner.invoke(cli, ["--path", str(cloned.repo_path), "add", str(path), "--sync"])
    name = path.stem
    result = runner.invoke(
        cli,
        [
            "--path",
            str(cloned.repo_path),
            "add-alias",
            name,
            "my-alias",
            "--sync" if sync else "--no-sync",
        ],
    )
    assert result.exit_code == 0, result.output
    assert "my-alias" in result.output

    with expectation:
        compare_repo_and_catalog(repo_cloned_bare, cloned)


@pytest.mark.parametrize(
    "sync,expectation",
    (
        (True, does_not_raise()),
        (False, pytest.raises(AssertionError)),
    ),
)
def test_remove_alias_sync(sync, expectation, runner, repo_cloned_bare, tmpdir):
    cloned = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("remove-alias-sync-test")
    )
    path = make_build_tgz(tmpdir, "to-alias")
    runner.invoke(cli, ["--path", str(cloned.repo_path), "add", str(path), "--sync"])
    name = path.stem
    runner.invoke(
        cli,
        ["--path", str(cloned.repo_path), "add-alias", name, "my-alias", "--sync"],
    )
    result = runner.invoke(
        cli,
        [
            "--path",
            str(cloned.repo_path),
            "remove-alias",
            "my-alias",
            "--sync" if sync else "--no-sync",
        ],
    )
    assert result.exit_code == 0, result.output
    assert "my-alias" in result.output

    with expectation:
        compare_repo_and_catalog(repo_cloned_bare, cloned)


def test_remove_entry_cascades_aliases(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "alias-p"])
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "alias-q"])

    result = runner.invoke(cli, ["--path", catalog_path, "remove", name])
    assert result.exit_code == 0, result.output

    catalog = Catalog.from_kwargs(path=catalog_path, init=False)
    assert catalog.list_aliases() == []
    catalog.assert_consistency()


# --- info command ---


def test_info_empty(runner, catalog_path):
    result = runner.invoke(cli, ["--path", catalog_path, "info"])
    assert result.exit_code == 0, result.output
    assert "path:" in result.output
    assert "commit:" in result.output
    assert "entries: 0" in result.output
    assert "aliases: 0" in result.output
    assert "(none)" in result.output


def test_info_populated(runner, catalog_path, data_dict):
    paths = [str(p) for p in data_dict.values()]
    runner.invoke(cli, ["--path", catalog_path, "add", *paths])
    result = runner.invoke(cli, ["--path", catalog_path, "info"])
    assert result.exit_code == 0, result.output
    assert f"entries: {len(data_dict)}" in result.output
    assert "aliases: 0" in result.output


def test_info_alias_count(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "alias-p"])
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "alias-q"])
    result = runner.invoke(cli, ["--path", catalog_path, "info"])
    assert result.exit_code == 0, result.output
    assert "aliases: 2" in result.output


def test_info_shows_remotes(runner, repo_cloned_bare, tmpdir):
    cloned = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("info-remote-test")
    )
    result = runner.invoke(cli, ["--path", str(cloned.repo_path), "info"])
    assert result.exit_code == 0, result.output
    assert "origin" in result.output


# --- list command ---


def test_list_empty(runner, catalog_path):
    result = runner.invoke(cli, ["--path", catalog_path, "list"])
    assert result.exit_code == 0, result.output
    assert "No entries." in result.output


def test_list_populated(runner, catalog_path, data_dict):
    paths = [str(p) for p in data_dict.values()]
    runner.invoke(cli, ["--path", catalog_path, "add", *paths])
    result = runner.invoke(cli, ["--path", catalog_path, "list"])
    assert result.exit_code == 0, result.output
    assert "No entries." not in result.output
    for p in data_dict.values():
        name = Path(p).name.removesuffix("".join(Path(p).suffixes))
        assert name in result.output


# --- list-aliases command ---


def test_list_aliases_invalid_path(runner):
    result = runner.invoke(cli, ["--path", "/nonexistent/path", "list-aliases"])
    assert result.exit_code != 0
    assert "init" in result.output


def test_list_aliases_empty(runner, catalog_path):
    result = runner.invoke(cli, ["--path", catalog_path, "list-aliases"])
    assert result.exit_code == 0, result.output
    assert "No aliases." in result.output


def test_list_aliases_populated(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "alias-a"])
    runner.invoke(cli, ["--path", catalog_path, "add-alias", name, "alias-b"])
    result = runner.invoke(cli, ["--path", catalog_path, "list-aliases"])
    assert result.exit_code == 0, result.output
    assert "alias-a" in result.output
    assert "alias-b" in result.output


# --- get command ---


def test_get_command(runner, catalog_path, data_dict, tmpdir):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    output_dir = str(Path(tmpdir).joinpath("export"))
    Path(output_dir).mkdir()
    result = runner.invoke(cli, ["--path", catalog_path, "get", name, "-o", output_dir])
    assert result.exit_code == 0, result.output
    assert "Exported to" in result.output


def test_get_default_output_dir(runner, catalog_path, data_dict):
    path = str(next(iter(data_dict.values())))
    runner.invoke(cli, ["--path", catalog_path, "add", path])
    name = Path(path).name.removesuffix("".join(Path(path).suffixes))
    with runner.isolated_filesystem():
        result = runner.invoke(cli, ["--path", catalog_path, "get", name])
        assert result.exit_code == 0, result.output
        assert "Exported to" in result.output


def test_get_nonexistent_entry(runner, catalog_path, tmpdir):
    output_dir = str(Path(tmpdir).joinpath("export"))
    Path(output_dir).mkdir()
    result = runner.invoke(
        cli, ["--path", catalog_path, "get", "nonexistent", "-o", output_dir]
    )
    assert result.exit_code != 0


# --- check command ---


def test_check_command(runner, catalog_path):
    result = runner.invoke(cli, ["--path", catalog_path, "check"])
    assert result.exit_code == 0, result.output
    assert "OK" in result.output


def test_check_catches_inconsistency(runner, catalog_path, tmpdir):
    catalog = Catalog.from_kwargs(path=catalog_path, init=False)
    tgz_path = write_tgz(
        Path(tmpdir).joinpath("build.tgz"),
        dict.fromkeys(REQUIRED_TGZ_NAMES, b""),
    )
    catalog_addition = CatalogAddition(BuildTgz(tgz_path), catalog)
    catalog_addition.ensure_dirs()
    entry_path = catalog_addition.catalog_entry.catalog_path
    with catalog.commit_context("bad commit"):
        shutil.copy(tgz_path, entry_path)
        catalog.repo.index.add((entry_path,))

    result = runner.invoke(cli, ["--path", catalog_path, "check"])
    assert result.exit_code != 0


def test_check_populated(runner, catalog_path, data_dict):
    paths = [str(p) for p in data_dict.values()]
    runner.invoke(cli, ["--path", catalog_path, "add", *paths])
    result = runner.invoke(cli, ["--path", catalog_path, "check"])
    assert result.exit_code == 0, result.output
    assert "OK" in result.output


# --- clone command ---


def test_clone_command(runner, catalog_populated, tmpdir):
    dest = str(Path(tmpdir).joinpath("cloned"))
    result = runner.invoke(
        cli, ["clone", str(catalog_populated.repo_path), "--path", dest]
    )
    assert result.exit_code == 0, result.output
    assert "Cloned to" in result.output
    assert Path(dest).exists()


def test_clone_name_and_path_mutually_exclusive(runner, catalog_populated):
    result = runner.invoke(
        cli,
        [
            "clone",
            str(catalog_populated.repo_path),
            "--name",
            "foo",
            "--path",
            "/tmp/bar",
        ],
    )
    assert result.exit_code != 0


def test_clone_derives_name_from_url(runner, catalog_populated, tmpdir, monkeypatch):
    clone_base = Path(tmpdir).joinpath("clone-base")
    clone_base.mkdir()
    monkeypatch.setattr(Catalog, "by_name_base_path", clone_base)
    result = runner.invoke(cli, ["clone", str(catalog_populated.repo_path)])
    assert result.exit_code == 0, result.output
    assert "Cloned to" in result.output


# --- push / pull / sync ---


def test_push_with_remote(runner, repo_cloned_bare, tmpdir):
    cloned = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("push-test")
    )
    compare_repo_and_catalog(repo_cloned_bare, cloned)
    before = repo_cloned_bare.head.commit.hexsha

    result = runner.invoke(cli, ["--path", str(cloned.repo_path), "push"])
    assert result.exit_code == 0, result.output
    assert "Pushed." in result.output
    middle = repo_cloned_bare.head.commit.hexsha
    assert before == middle

    # check commit
    path = make_build_tgz(tmpdir, "to-add")
    result = runner.invoke(
        cli, ["--path", cloned.repo_path, "add", str(path), "--no-sync"]
    )
    assert result.exit_code == 0, result.output
    assert "Added" in result.output
    middle = repo_cloned_bare.head.commit.hexsha
    assert before == middle
    with pytest.raises(AssertionError):
        compare_repo_and_catalog(repo_cloned_bare, cloned)

    result = runner.invoke(cli, ["--path", str(cloned.repo_path), "push"])
    assert result.exit_code == 0, result.output
    assert "Pushed." in result.output
    after = repo_cloned_bare.head.commit.hexsha
    assert before != after
    compare_repo_and_catalog(repo_cloned_bare, cloned)


def test_pull_with_remote(runner, repo_cloned_bare, tmpdir):
    cloned = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("pull-test")
    )
    pusher = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("pusher")
    )
    before = cloned.repo.head.commit.hexsha

    result = runner.invoke(cli, ["--path", str(cloned.repo_path), "pull"])
    assert result.exit_code == 0, result.output
    assert "Pulled." in result.output
    #
    middle = cloned.repo.head.commit.hexsha
    assert before == middle
    assert middle == pusher.repo.head.commit.hexsha

    path = make_build_tgz(tmpdir, "to-add")
    pusher.add(path, sync=True)
    middle = cloned.repo.head.commit.hexsha
    assert before == middle
    assert middle != pusher.repo.head.commit.hexsha

    result = runner.invoke(cli, ["--path", str(cloned.repo_path), "pull"])
    assert result.exit_code == 0, result.output
    assert "Pulled." in result.output
    #
    after = cloned.repo.head.commit.hexsha
    assert before != after
    assert after == pusher.repo.head.commit.hexsha


def test_sync_with_remote(runner, repo_cloned_bare, tmpdir):
    cloned = Catalog.clone_from(
        repo_cloned_bare.working_dir, Path(tmpdir).joinpath("sync-test")
    )
    result = runner.invoke(cli, ["--path", str(cloned.repo_path), "sync"])
    assert result.exit_code == 0, result.output
    assert "Synced." in result.output


# --- CLI option validation ---


def test_help(runner):
    result = runner.invoke(cli, ["--help"])
    assert result.exit_code == 0
    assert "Manage xorq build-artifact catalogs." in result.output


def test_subcommand_help(runner):
    for cmd in (
        "init",
        "add",
        "add-alias",
        "remove",
        "remove-alias",
        "info",
        "list",
        "list-aliases",
        "get",
        "push",
        "pull",
        "sync",
        "clone",
        "check",
    ):
        result = runner.invoke(cli, [cmd, "--help"])
        assert result.exit_code == 0, f"{cmd} --help failed"


def test_name_and_path_mutually_exclusive(runner, catalog_path, tmpdir, monkeypatch):
    monkeypatch.setattr(Catalog, "by_name_base_path", Path(tmpdir))
    runner.invoke(cli, ["--name", "my-catalog", "init"])
    result = runner.invoke(
        cli, ["--name", "my-catalog", "--path", catalog_path, "list"]
    )
    assert result.exit_code != 0


def test_list_invalid_path(runner):
    result = runner.invoke(cli, ["--path", "/nonexistent/path", "list"])
    assert result.exit_code != 0
    assert "init" in result.output


def test_missing_catalog_hint_includes_path(runner, tmpdir):
    missing = str(Path(tmpdir).joinpath("no-such-catalog"))
    result = runner.invoke(cli, ["--path", missing, "list"])
    assert result.exit_code != 0
    assert f"--path {missing}" in result.output
    assert "init" in result.output


def test_missing_catalog_hint_includes_name(runner, tmpdir, monkeypatch):
    monkeypatch.setattr(Catalog, "by_name_base_path", Path(tmpdir))
    result = runner.invoke(cli, ["--name", "my-catalog", "list"])
    assert result.exit_code != 0
    assert "--name my-catalog" in result.output
    assert "init" in result.output


# --- TUI validation ---


def test_tui_missing_catalog_shows_hint(runner, tmpdir):
    missing = str(Path(tmpdir).joinpath("no-such-catalog"))
    result = runner.invoke(cli, ["--path", missing, "tui"])
    assert result.exit_code != 0
    assert "init" in result.output


def test_tui_missing_catalog_with_name(runner, tmpdir):
    result = runner.invoke(cli, ["--name", "no-such-catalog-xyz", "tui"])
    assert result.exit_code != 0
    assert "init" in result.output
