#!/usr/bin/python3
"""Tests for sbo_create.main.SBoCreate._init_templates and .arguments()."""

import argparse
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from sbo_create.main import SBoCreate
from sbo_create.models import AppContext


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_parsed(command: str, **kwargs: object) -> argparse.Namespace:
    """Return an argparse.Namespace for the given subcommand with sensible defaults."""
    defaults: dict[str, object] = {'command': command, 'no_color': False}
    if command == 'create':
        defaults.update(dict(
            prgnam=None, prgnam_pos=None, prg_version=None, template=None,
            arm64=False, output=None, dry_run=False, strip_comments=False,
        ))
    elif command == 'download':
        defaults.update(dict(check_urls=False))
    elif command == 'check':
        defaults.update(dict(names=[]))
    elif command == 'search':
        defaults.update(dict(names=[]))
    elif command == 'validate':
        defaults.update(dict(md5=False, fix=False, strict=False, all=False,
                             maintainer=None, path=None))
    elif command == 'bump':
        defaults.update(dict(version='1.0', with_sources=False))
    elif command == 'package':
        defaults.update(dict(output=None))
    elif command == 'edit':
        defaults.update(dict(editor=None))
    elif command == 'rename':
        defaults.update(dict(name='newapp'))
    elif command == 'maintainer':
        defaults.update(dict(show=False))
    elif command == 'fetch':
        defaults.update(dict(name=[], slackware_version=None, list=False, file=None, maintainer=False, wide=False))
    defaults.update(kwargs)
    return argparse.Namespace(**defaults)


def make_sbo(ctx: AppContext) -> SBoCreate:
    """Construct a bare SBoCreate without calling __init__."""
    obj = SBoCreate.__new__(SBoCreate)
    obj.ctx = ctx
    obj.files = MagicMock()
    obj.resolver = MagicMock()
    obj.misc = MagicMock()
    obj.download = MagicMock()
    obj.fetch = MagicMock()
    obj.git = MagicMock()
    obj.validate = MagicMock()
    obj.bump = MagicMock()
    obj.scan = MagicMock()
    return obj


# ---------------------------------------------------------------------------
# _init_templates
# ---------------------------------------------------------------------------

def test_init_templates_exists_no_raise(ctx: AppContext) -> None:
    """When templates_path exists, _init_templates does nothing."""
    assert ctx.templates_path.exists(), "fixture must provide an existing templates dir"
    sbo = make_sbo(ctx)
    sbo._init_templates()  # must not raise


def test_init_templates_missing_prints_and_raises(ctx: AppContext, tmp_path: Path,
                                                   capsys: pytest.CaptureFixture) -> None:
    ctx.templates_path = tmp_path / 'nonexistent_templates'
    sbo = make_sbo(ctx)

    with pytest.raises(SystemExit) as exc_info:
        sbo._init_templates()

    assert exc_info.value.code == 1
    captured = capsys.readouterr()
    assert 'Templates not found' in captured.out


# ---------------------------------------------------------------------------
# arguments — create
# ---------------------------------------------------------------------------

def test_arguments_sets_prg_name(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    parsed = make_parsed('create', prgnam='newname')
    sbo.arguments(parsed)
    assert ctx.prg_name == 'newname'


def test_arguments_sets_version(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    parsed = make_parsed('create', prg_version='2.0')
    sbo.arguments(parsed)
    assert ctx.info.version == '2.0'


def test_arguments_sets_template(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    parsed = make_parsed('create', template='cmake')
    sbo.arguments(parsed)
    assert ctx.template == 'cmake'


def test_arguments_arm64_calls_init_info_text_arm(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    parsed = make_parsed('create', arm64=True)
    sbo.arguments(parsed)
    sbo.files.init_info_text_arm.assert_called_once()


def test_arguments_create_calls_create_files(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    parsed = make_parsed('create', template='autotools')
    sbo.arguments(parsed)
    sbo.files.create_files.assert_called_once_with(dry_run=False)


def test_arguments_create_dry_run(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    parsed = make_parsed('create', dry_run=True)
    sbo.arguments(parsed)
    sbo.files.create_files.assert_called_once_with(dry_run=True)


def test_arguments_create_output_sets_current_folder(ctx: AppContext,
                                                      tmp_path: Path) -> None:
    sbo = make_sbo(ctx)
    out = tmp_path / 'output'
    parsed = make_parsed('create', output=str(out))
    sbo.arguments(parsed)
    assert ctx.current_folder == out.resolve()


# ---------------------------------------------------------------------------
# arguments — other subcommands
# ---------------------------------------------------------------------------

def test_arguments_download_calls_sources_download(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('download'))
    sbo.download.sources_download.assert_called_once()


def test_arguments_check_calls_is_the_sbo_exists(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('check', names=['vim', 'emacs']))
    sbo.misc.is_the_sbo_exists.assert_called_once_with(['vim', 'emacs'])


def test_arguments_info_calls_show_info(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('info'))
    sbo.misc.show_info.assert_called_once()


def test_arguments_search_calls_search_sbo_packages(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('search', names=['vim']))
    sbo.misc.search_sbo_packages.assert_called_once_with(['vim'])


def test_arguments_clean_calls_clean_sources(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    with pytest.raises(SystemExit) as exc_info:
        sbo.arguments(make_parsed('clean'))
    sbo.misc.clean_sources.assert_called_once()
    assert exc_info.value.code == 0


def test_arguments_download_check_urls(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('download', check_urls=True))
    sbo.download.sources_download.assert_called_once_with(check_urls=True)


def test_arguments_list_calls_list_templates(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('list'))
    sbo.misc.list_templates.assert_called_once()


def test_arguments_validate_calls_validate_info(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('validate'))
    sbo.validate.validate_info.assert_called_once()


def test_arguments_validate_all_calls_validate_all(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('validate', all=True))
    sbo.validate.validate_all.assert_called_once_with(
        check_md5=False, fix=False, strict=False, maintainer=None, path=None)


def test_arguments_validate_fix_passed(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('validate', fix=True))
    sbo.validate.validate_info.assert_called_once_with(
        check_md5=False, fix=True, strict=False, path=None)


def test_arguments_validate_strict_passed(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('validate', strict=True))
    sbo.validate.validate_info.assert_called_once_with(
        check_md5=False, fix=False, strict=True, path=None)


def test_arguments_bump_calls_bump_version(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('bump', version='3.0', with_sources=True))
    sbo.bump.bump_version.assert_called_once_with('3.0', with_sources=True)


def test_arguments_package_calls_create_package(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('package'))
    sbo.misc.create_package.assert_called_once()


def test_arguments_update_calls_update_templates(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('update'))
    sbo.misc.update_templates.assert_called_once()


def test_arguments_edit_calls_edit_files(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('edit'))
    sbo.misc.edit_files.assert_called_once_with(editor=None)


def test_arguments_edit_passes_editor_override(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('edit', editor='subl'))
    sbo.misc.edit_files.assert_called_once_with(editor='subl')


def test_arguments_rename_calls_rename_package(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('rename', name='newapp'))
    sbo.misc.rename_package.assert_called_once_with('newapp')


def test_arguments_maintainer_calls_configure_maintainer(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('maintainer', show=False))
    sbo.misc.configure_maintainer.assert_called_once_with(show=False)


def test_arguments_maintainer_show(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('maintainer', show=True))
    sbo.misc.configure_maintainer.assert_called_once_with(show=True)


# ---------------------------------------------------------------------------
# _warn_if_sbo_exists
# ---------------------------------------------------------------------------

def test_warn_if_sbo_exists_not_in_cache_proceeds(ctx: AppContext) -> None:
    """Package not in SBo cache: no prompt, no exit."""
    sbo = make_sbo(ctx)
    sbo.ctx.sbo_packages = {'other-pkg': 'system  desc'}
    sbo._warn_if_sbo_exists()  # should not raise


def test_warn_if_sbo_exists_empty_cache_proceeds(ctx: AppContext) -> None:
    """Empty SBo cache: no prompt, no exit."""
    sbo = make_sbo(ctx)
    sbo._warn_if_sbo_exists()  # should not raise


def test_warn_if_sbo_exists_user_confirms(ctx: AppContext) -> None:
    """Package in SBo cache and user confirms: continues."""
    sbo = make_sbo(ctx)
    sbo.ctx.sbo_packages = {ctx.prg_name: 'system  some desc'}
    with patch('builtins.input', return_value='y'):
        sbo._warn_if_sbo_exists()  # should not raise


def test_warn_if_sbo_exists_user_declines(ctx: AppContext) -> None:
    """Package in SBo cache and user declines: exits 0."""
    sbo = make_sbo(ctx)
    sbo.ctx.sbo_packages = {ctx.prg_name: 'system  some desc'}
    with patch('builtins.input', return_value='n'), pytest.raises(SystemExit) as exc_info:
        sbo._warn_if_sbo_exists()
    assert exc_info.value.code == 0


# ---------------------------------------------------------------------------
# arguments — fetch subcommand
# ---------------------------------------------------------------------------

def test_arguments_fetch_name_calls_fetch_package(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('fetch', name=['vim']))
    sbo.fetch.fetch_package.assert_called_once_with('vim', slack_ver=None)


def test_arguments_fetch_list_calls_list_files(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('fetch', name=['vim'], list=True))
    sbo.fetch.fetch_package.assert_called_once_with('vim', slack_ver=None, list_files=True)


def test_arguments_fetch_view_single_file(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('fetch', name=['vim'], file=['README']))
    sbo.fetch.fetch_package.assert_called_once_with('vim', slack_ver=None, read_files=['README'])


def test_arguments_fetch_view_multiple_files(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('fetch', name=['vim'], file=['README', 'vim.info']))
    sbo.fetch.fetch_package.assert_called_once_with('vim', slack_ver=None,
                                                    read_files=['README', 'vim.info'])


def test_arguments_fetch_maintainer_explicit_name(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('fetch', name=['Alice'], maintainer=True))
    sbo.fetch.list_maintainer_packages.assert_called_once_with('Alice', wide=False)


def test_arguments_fetch_maintainer_wide(ctx: AppContext) -> None:
    sbo = make_sbo(ctx)
    sbo.arguments(make_parsed('fetch', name=['Alice'], maintainer=True, wide=True))
    sbo.fetch.list_maintainer_packages.assert_called_once_with('Alice', wide=True)
