#!/usr/bin/python3
"""Tests for instance methods of sbo_create.handlers.files.FileHandler."""

import hashlib
import sys
from io import StringIO
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from sbo_create.handlers.files import FileHandler
from sbo_create.models import AppContext, InfoData, MaintainerData, set_x86_labels


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def fh(ctx: AppContext, tmp_path: Path) -> FileHandler:
    """FileHandler with current_folder and config_path pointing at tmp_path."""
    ctx.current_folder = tmp_path
    ctx.config_path = tmp_path / 'config'
    ctx.config_path.mkdir(parents=True, exist_ok=True)
    handler = FileHandler(ctx)
    return handler


# ---------------------------------------------------------------------------
# ensure_config_dir
# ---------------------------------------------------------------------------

def test_ensure_config_dir_creates_new_dir(ctx: AppContext, tmp_path: Path) -> None:
    ctx.current_folder = tmp_path
    ctx.config_path = tmp_path / 'new_config_dir'
    assert not ctx.config_path.exists()
    handler = FileHandler(ctx)
    handler.ensure_config_dir()
    assert ctx.config_path.is_dir()


def test_ensure_config_dir_noop_if_exists(fh: FileHandler) -> None:
    assert fh.ctx.config_path.is_dir()
    # Should not raise even when directory already exists
    fh.ensure_config_dir()
    assert fh.ctx.config_path.is_dir()


# ---------------------------------------------------------------------------
# init_info_text / init_info_text_arm
# ---------------------------------------------------------------------------

def test_init_info_text_sets_x86_labels(fh: FileHandler) -> None:
    fh.init_info_text()
    labels = fh.ctx.info.labels
    assert len(labels) > 3
    assert 'DOWNLOAD=' in labels[3]


def test_init_info_text_arm_sets_arm64_labels(fh: FileHandler) -> None:
    fh.init_info_text_arm()
    labels = fh.ctx.info.labels
    assert len(labels) > 5
    assert 'ARM64' in labels[5]


def test_init_info_text_arm_sets_arm_support_flag(fh: FileHandler) -> None:
    fh.init_info_text_arm()
    assert fh.ctx.info.arm_support is True


# ---------------------------------------------------------------------------
# read_info_file
# ---------------------------------------------------------------------------

def test_read_info_file_no_file_does_not_raise(fh: FileHandler) -> None:
    fh.read_info_file()  # file doesn't exist – should silently return


def test_read_info_file_reads_version_and_homepage(fh: FileHandler, tmp_path: Path) -> None:
    info_content = (
        'PRGNAM="myapp"\n'
        'VERSION="3.0"\n'
        'HOMEPAGE="https://myapp.example.com"\n'
        'DOWNLOAD="https://example.com/myapp-3.0.tar.gz"\n'
        'MD5SUM="abc123"\n'
        'DOWNLOAD_x86_64=""\n'
        'MD5SUM_x86_64=""\n'
        'REQUIRES=""\n'
        'MAINTAINER="Jane"\n'
        'EMAIL="jane@example.com"\n'
    )
    info_file = tmp_path / 'myapp.info'
    info_file.write_text(info_content, encoding='utf-8')
    fh.ctx.prg_name = 'myapp'

    set_x86_labels(fh.ctx.info)
    fh.read_info_file()

    assert fh.ctx.info.version == '3.0'
    assert fh.ctx.info.homepage == 'https://myapp.example.com'


def test_read_info_file_arm64_content_sets_arm_labels(fh: FileHandler, tmp_path: Path) -> None:
    info_content = (
        'PRGNAM="myapp"\n'
        'VERSION="1.0"\n'
        'HOMEPAGE="https://example.com"\n'
        'DOWNLOAD="UNSUPPORTED"\n'
        'MD5SUM=""\n'
        'DOWNLOAD_ARM64="https://example.com/myapp-1.0-arm.tar.gz"\n'
        'MD5SUM_ARM64="deadbeef"\n'
        'REQUIRES=""\n'
        'MAINTAINER="Jane"\n'
        'EMAIL="jane@example.com"\n'
    )
    info_file = tmp_path / 'myapp.info'
    info_file.write_text(info_content, encoding='utf-8')
    fh.ctx.prg_name = 'myapp'

    # Start with x86 labels so the ARM64 detection path is exercised
    set_x86_labels(fh.ctx.info)
    fh.read_info_file()

    assert fh.ctx.info.arm_support is True


# ---------------------------------------------------------------------------
# read_maintainer_file
# ---------------------------------------------------------------------------

def test_read_maintainer_file_no_file_does_not_raise(fh: FileHandler) -> None:
    fh.read_maintainer_file()  # file absent – silently returns


def test_read_maintainer_file_valid_config(fh: FileHandler, tmp_path: Path) -> None:
    config_content = (
        'NAME=John Doe\n'
        'EMAIL=john@example.com\n'
        'LOCATION=Nowhere\n'
        'EDITOR=vim\n'
        'EDITOR_OPTIONS=\n'
        'SBO_VERSION=15.0\n'
    )
    config_file = fh.ctx.config_path / fh.ctx.config_file
    config_file.write_text(config_content, encoding='utf-8')

    fh.read_maintainer_file()

    assert fh.ctx.maintainer.name == 'John Doe'
    assert fh.ctx.maintainer.email == 'john@example.com'
    assert fh.ctx.maintainer.editor == 'vim'
    assert fh.ctx.maintainer.sbo_repo_version == '15.0'


def test_read_maintainer_file_missing_key_warns(fh: FileHandler,
                                                 capsys: pytest.CaptureFixture) -> None:
    """Missing keys produce a per-field warning; present keys are still loaded."""
    config_content = (
        'NAME=John Doe\n'
        'EMAIL=john@example.com\n'
        'LOCATION=Nowhere\n'
        'EDITOR=vim\n'
    )
    config_file = fh.ctx.config_path / fh.ctx.config_file
    config_file.write_text(config_content, encoding='utf-8')

    fh.read_maintainer_file()

    captured = capsys.readouterr()
    assert "Warning: 'EDITOR_OPTIONS' not found" in captured.err
    assert "Warning: 'SBO_VERSION' not found" in captured.err
    assert fh.ctx.maintainer.name == 'John Doe'
    assert fh.ctx.maintainer.editor == 'vim'


def test_read_maintainer_file_value_with_equals(fh: FileHandler) -> None:
    """Values that contain '=' are read correctly."""
    config_content = (
        'NAME=John Doe\n'
        'EMAIL=john@example.com\n'
        'LOCATION=Nowhere\n'
        'EDITOR=vim\n'
        'EDITOR_OPTIONS=-c set nu=1\n'
        'SBO_VERSION=15.0\n'
    )
    config_file = fh.ctx.config_path / fh.ctx.config_file
    config_file.write_text(config_content, encoding='utf-8')

    fh.read_maintainer_file()

    assert fh.ctx.maintainer.editor_options == '-c set nu=1'


# ---------------------------------------------------------------------------
# source_check_sum
# ---------------------------------------------------------------------------

def test_source_check_sum_existing_file(fh: FileHandler, tmp_path: Path) -> None:
    data = b'hello world'
    source_file = tmp_path / 'source.tar.gz'
    source_file.write_bytes(data)

    result = fh.source_check_sum('source.tar.gz')

    expected = hashlib.md5(data).hexdigest()
    assert result == expected


def test_source_check_sum_missing_file_returns_empty(fh: FileHandler,
                                                      capsys: pytest.CaptureFixture) -> None:
    result = fh.source_check_sum('nonexistent.tar.gz')
    assert result == ''
    captured = capsys.readouterr()
    assert captured.err == ''

# ---------------------------------------------------------------------------
# create_files
# ---------------------------------------------------------------------------

def test_create_files_with_template_raises_systemexit(fh: FileHandler, tmp_path: Path) -> None:
    fh.ctx.prg_name = 'myapp'
    fh.ctx.current_folder = tmp_path
    fh.ctx.template = 'python'

    with pytest.raises(SystemExit):
        fh.create_files()


def test_create_files_with_template_creates_expected_files(fh: FileHandler,
                                                            tmp_path: Path) -> None:
    fh.ctx.prg_name = 'myapp'
    fh.ctx.current_folder = tmp_path
    fh.ctx.template = 'python'

    with pytest.raises(SystemExit):
        fh.create_files()

    pkg_dir = tmp_path / 'myapp'
    assert (pkg_dir / 'myapp.SlackBuild').exists()
    assert (pkg_dir / 'myapp.info').exists()
    assert (pkg_dir / 'slack-desc').exists()
    assert (pkg_dir / 'README').exists()


def test_create_files_no_template_raises_systemexit(fh: FileHandler, tmp_path: Path) -> None:
    fh.ctx.prg_name = 'myapp'
    fh.ctx.current_folder = tmp_path
    fh.ctx.template = ''

    with pytest.raises(SystemExit):
        fh.create_files()


# ---------------------------------------------------------------------------
# create_interactive
# ---------------------------------------------------------------------------

def test_create_interactive_creates_files(fh: FileHandler, tmp_path: Path) -> None:
    """Interactive mode with valid input creates expected files."""
    fh.ctx.current_folder = tmp_path
    inputs = iter(['mypkg', '2.0', 'autotools', 'n', 'n', ''])
    with patch('builtins.input', side_effect=lambda _='': next(inputs)):
        with pytest.raises(SystemExit) as exc_info:
            fh.create_interactive()
    assert exc_info.value.code == 0
    pkg_dir = tmp_path / 'mypkg'
    assert (pkg_dir / 'mypkg.SlackBuild').exists()
    assert (pkg_dir / 'mypkg.info').exists()


def test_create_interactive_uses_defaults(fh: FileHandler, tmp_path: Path) -> None:
    """Empty input keeps default package name and version."""
    fh.ctx.current_folder = tmp_path
    inputs = iter(['', '', 'autotools', 'n', 'n', ''])
    with patch('builtins.input', side_effect=lambda _='': next(inputs)):
        with pytest.raises(SystemExit):
            fh.create_interactive()
    assert fh.ctx.prg_name == 'myapp'


def test_create_interactive_invalid_template_exits(fh: FileHandler, tmp_path: Path) -> None:
    """Unknown template name exits with code 1."""
    fh.ctx.current_folder = tmp_path
    inputs = iter(['mypkg', '1.0', 'invalid_template', 'n', 'n', ''])
    with patch('builtins.input', side_effect=lambda _='': next(inputs)):
        with pytest.raises(SystemExit) as exc_info:
            fh.create_interactive()
    assert exc_info.value.code == 1


def test_create_interactive_keyboard_interrupt_exits(fh: FileHandler, tmp_path: Path) -> None:
    """KeyboardInterrupt during input exits with code 0."""
    fh.ctx.current_folder = tmp_path
    with patch('builtins.input', side_effect=KeyboardInterrupt):
        with pytest.raises(SystemExit) as exc_info:
            fh.create_interactive()
    assert exc_info.value.code == 0


def test_create_interactive_arm64_sets_arm_labels(fh: FileHandler, tmp_path: Path) -> None:
    """Answering 'y' to ARM64 prompt sets ARM labels."""
    fh.ctx.current_folder = tmp_path
    inputs = iter(['mypkg', '1.0', 'autotools', 'y', 'n', ''])
    with patch('builtins.input', side_effect=lambda _='': next(inputs)):
        with pytest.raises(SystemExit):
            fh.create_interactive()
    assert fh.ctx.info.arm_support is True


def test_create_interactive_strip_comments(fh: FileHandler, tmp_path: Path) -> None:
    """Answering 'y' to Strip comments sets strip_comments flag."""
    fh.ctx.current_folder = tmp_path
    inputs = iter(['mypkg', '1.0', 'autotools', 'n', 'y', ''])
    with patch('builtins.input', side_effect=lambda _='': next(inputs)):
        with pytest.raises(SystemExit):
            fh.create_interactive()
    assert fh.ctx.strip_comments is True


def test_create_interactive_calls_warn_callback(fh: FileHandler, tmp_path: Path) -> None:
    """warn_callback is called after name/options are set, before create_files."""
    fh.ctx.current_folder = tmp_path
    inputs = iter(['mypkg', '1.0', 'autotools', 'n', 'n', ''])
    callback = MagicMock()
    with patch('builtins.input', side_effect=lambda _='': next(inputs)):
        with pytest.raises(SystemExit):
            fh.create_interactive(warn_callback=callback)
    callback.assert_called_once()
