# pylint: disable=protected-access

import gzip
from textwrap import dedent
import os
import tempfile

import pytest

from utils import (
    dpkg_status_file,
    find_test_file,
    install_yaml_package,
    make_config,
    make_dpkg_status,
    package_from_deb_file,
    package_from_yaml_file,
    setup_filterer,
)
from apt_listchanges.ALCSeenDb import SeenDb
from apt_listchanges.ALChacks import _
from apt_listchanges.apt_listchanges import (
    Filterer,
    doit,
    process_pkg,
    process_pkgs,
)
from apt_listchanges.frontends import Frontend


def test_create_package_from_yaml():
    pkg = package_from_yaml_file('basic_package.yaml')
    assert pkg.binary == 'basic-package'
    assert pkg.source == 'basic-package'
    assert pkg.version == '1.1-1'
    assert pkg.arch == 'amd64'
    assert '/usr/share/doc/basic-package/changelog.gz' in pkg.files
    assert '/usr/share/doc/basic-package/NEWS.Debian' in pkg.files
    assert pkg.path.endswith('tests/package_files/basic_package.yaml')


def test_create_package_from_deb():
    pkg = package_from_deb_file('chrome-gnome-shell_42.1-4_all.deb')
    assert pkg.binary == 'chrome-gnome-shell'
    assert pkg.source == 'gnome-browser-connector'
    assert pkg.version == '42.1-4'
    assert pkg.arch == 'all'
    assert pkg.path.endswith(
        'tests/package_files/chrome-gnome-shell_42.1-4_all.deb')


def test_extract_contents_from_yaml():
    pkg = package_from_yaml_file('basic_package.yaml')
    filenames = pkg.news_filenames + pkg.changelog_filenames
    tmp_dir = pkg._extract_contents(filenames)
    found_changelog = None
    found_news = None
    found_others = []
    for dirpath, dirnames, filenames in os.walk(tmp_dir):
        for filename in filenames:
            if filename == 'changelog.gz':
                assert not found_changelog
                found_changelog = os.path.join(dirpath, filename)
            elif filename == 'NEWS.Debian':
                assert not found_news
                found_news = os.path.join(dirpath, filename)
            else:
                found_others.append(os.path.join(dirpath, filename))
    assert not found_others
    assert found_changelog
    assert found_news
    assert gzip.open(found_changelog, 'rt', encoding='utf-8').read() == \
        dedent('''\
    basic-package (1.1) unstable; urgency=medium

      Here's what changed in version 1.1

     -- Debian Hero <dev@example.com>  Sat, 31 Dec 2022 12:34:56 +0000

    basic-package (1.0) unstable; urgency=medium

      This is the first version of basic-package!

     -- Prior Debian Hero <olddev@example.com>  Fri, 6 May 2022 01:23:45 -0400
    ''')
    # pylint: disable=consider-using-with
    assert open(found_news, 'r', encoding='utf-8').read() == dedent('''\
    basic-package (1.1) unstable; urgency=low

      This must be something not terribly exciting about basic-package.

     -- Boring Dude <bdev@example.com>  Sat, 31 Dec 2022 11:22:33 +0200

    basic-package (1.0) unstable; urgency=low

      Who says packages have to be useful?

     -- Obstinate Dude <odev@example.com>  Fri, 6 May 2022 10:11:12 +0500
    ''')


def test_extract_contents_from_deb():
    pkg = package_from_deb_file('chrome-gnome-shell_42.1-4_all.deb')
    tmp_dir = pkg._extract_contents(pkg.changelog_filenames)
    found_changelog = None
    found_others = []
    for dirpath, dirnames, filenames in os.walk(tmp_dir):
        for filename in filenames:
            if filename == 'changelog.Debian.gz':
                assert not found_changelog
                found_changelog = os.path.join(dirpath, filename)
            else:
                found_others.append(os.path.join(dirpath, filename))
    assert not found_others
    assert found_changelog
    changelog_text = gzip.open(found_changelog, 'rt', encoding='utf-8').read()
    assert changelog_text.startswith(
        'gnome-browser-connector (42.1-4) unstable; urgency=medium\n')
    assert changelog_text.endswith('# To read the complete changelog use '
                                   '`apt changelog chrome-gnome-shell`.\n')


def test_mock_install_package(fs):
    fs.add_real_directory(os.path.dirname(__file__))
    install_yaml_package(fs, 'basic_package.yaml')
    control_parser = make_dpkg_status(dpkg_status_file)
    control_stanza = control_parser.find('Package', 'basic-package')[0]
    assert control_stanza
    assert control_stanza.Package == 'basic-package'
    assert control_stanza.source == 'basic-package'
    assert control_stanza.Version == '1.1-1'
    assert control_stanza.installed
    assert control_stanza.Architecture == 'amd64'
    assert gzip.open('/usr/share/doc/basic-package/changelog.gz', 'rt',
                     encoding='utf-8').read().endswith(
                         'Fri, 6 May 2022 01:23:45 -0400\n')
    # pylint: disable=consider-using-with
    assert open('/usr/share/doc/basic-package/NEWS.Debian', 'rt',
                encoding='utf-8').read().endswith(
                    'Fri, 6 May 2022 10:11:12 +0500\n')


def test_extract_changes():
    pkg = package_from_yaml_file('basic_package.yaml')
    (news, changelog) = pkg.extract_changes('both')
    # For some inexplicable reason, pylint has decided that `news` and
    # `changelog` are bools and therefore they don't have a member named
    # `changes`. They most definitely are not bools, but I have no idea what
    # it's thinking our how to fix it, so I'm disabling the warning.
    # pylint: disable=no-member
    assert 'basic-package (1.1)' in news.changes
    assert 'basic-package (1.0)' in news.changes
    assert 'basic-package (1.1)' in changelog.changes
    assert 'basic-package (1.0)' in changelog.changes
    (news, changelog) = pkg.extract_changes(
        'news', filterer=setup_filterer())
    assert news
    assert not changelog
    (news, changelog) = pkg.extract_changes(
        'changelogs', filterer=setup_filterer())
    assert not news
    assert changelog


def test_extract_via_installed(fs):
    fs.add_real_directory(os.path.dirname(__file__))
    install_yaml_package(fs, 'basic_package.yaml')
    pkg = package_from_yaml_file('basic_package_1.2.yaml')
    (news, changelog) = pkg.extract_changes_via_installed('both')
    # pylint: disable=no-member
    assert 'basic-package (1.1)' in news.changes
    assert 'basic-package (1.2)' not in news.changes
    assert 'basic-package (1.1)' in changelog.changes
    assert 'basic-package (1.2)' not in changelog.changes


def test_extract_via_apt():
    pkg = package_from_yaml_file('basic_package.yaml')
    changelog = pkg.extract_changes_via_apt()
    # pylint: disable=no-member
    assert 'basic-package (0.9)' in changelog.changes


def test_basic_upgrade(fs):
    fs.add_real_directory(os.path.dirname(__file__))
    install_yaml_package(fs, 'basic_package.yaml')
    config = make_config()
    seen_db = SeenDb()
    status = make_dpkg_status(dpkg_status_file)
    notes = []
    filterer = Filterer(config, seen_db)
    pkg = package_from_yaml_file('basic_package_1.2.yaml', filterer=filterer)
    (news, changelog) = process_pkg(config, seen_db, status, notes, pkg)
    assert 'basic-package (1.2)' in news.changes
    assert 'basic-package (1.1)' not in news.changes
    assert 'basic-package (1.2)' in changelog.changes
    assert 'basic-package (1.1)' not in changelog.changes


def test_two_packages():
    config = make_config({'show_all': True})
    frontend = Frontend(config, 2)
    seen_db = SeenDb()
    filterer = Filterer(config, seen_db)
    pkgs = [package_from_yaml_file(filename, filterer=filterer)
            for filename in ('basic_package.yaml', 'other_package.yaml')]
    (news, changelog) = process_pkgs(config, frontend, seen_db, pkgs)
    assert 'basic-package (1.1)' in news
    assert 'other-package (2.0-1)' in news
    assert 'basic-package (1.1)' in changelog
    assert 'other-package (2.0-1)' in changelog


def test_same_source_together():
    config = make_config({'show_all': True})
    frontend = Frontend(config, 2)
    seen_db = SeenDb()
    filterer = Filterer(config, seen_db)
    pkgs = [package_from_yaml_file(filename, filterer=filterer)
            for filename in ('foo-bin1-2.yaml', 'foo-bin2-2.yaml')]
    (news, changelog) = process_pkgs(config, frontend, seen_db, pkgs)
    assert news.count('foo (2.0-1)') == 1
    assert changelog.count('foo (2.0-1)') == 1


def test_same_source_separate(fs):
    fs.add_real_directory(os.path.dirname(__file__))
    install_yaml_package(fs, 'foo-bin1-1.yaml')
    install_yaml_package(fs, 'foo-bin2-1.yaml')
    config = make_config()
    frontend = Frontend(config, 2)
    seen_db = SeenDb()
    filterer = Filterer(config, seen_db)
    pkgs = [package_from_yaml_file(filename, filterer=filterer)
            for filename in ('foo-bin1-2.yaml', 'foo-bin2-3.yaml')]
    (news, changelog) = process_pkgs(config, frontend, seen_db, [pkgs[0]])
    assert 'foo (2.0-1)' in news
    assert 'foo (2.0-1)' in changelog
    (news, changelog) = process_pkgs(config, frontend, seen_db, [pkgs[1]])
    assert 'foo (2.0-1)' not in news
    assert 'foo (2.0-1)' not in changelog
    assert 'foo (3.0-1)' in news
    assert 'foo (3.0-1)' in changelog


def test_latest():
    config = make_config({'latest': 1})
    pkg = package_from_yaml_file('basic_package.yaml', config=config)
    (news, changelog) = pkg.extract_changes('both')
    # pylint: disable=no-member
    assert 'basic-package (1.1)' in news.changes
    assert 'basic-package (1.0)' not in news.changes
    assert 'basic-package (1.1)' in changelog.changes
    assert 'basic-package (1.0)' not in changelog.changes


def test_since():
    config = make_config({'since': '1.0'})
    pkg = package_from_yaml_file('basic_package.yaml', config=config)
    (news, changelog) = pkg.extract_changes('both')
    # pylint: disable=no-member
    assert 'basic-package (1.1)' in news.changes
    assert 'basic-package (1.0)' not in news.changes
    assert 'basic-package (1.1)' in changelog.changes
    assert 'basic-package (1.0)' not in changelog.changes


def test_show_all(fs):
    fs.add_real_directory(os.path.dirname(__file__))
    install_yaml_package(fs, 'basic_package.yaml')
    config = make_config({'show_all': True})
    seen_db = SeenDb()
    status = make_dpkg_status(dpkg_status_file)
    notes = []
    filterer = Filterer(config, seen_db)
    pkg = package_from_yaml_file('basic_package_1.2.yaml', filterer=filterer)
    (news, changelog) = process_pkg(config, seen_db, status, notes, pkg)
    assert 'basic-package (1.2)' in news.changes
    assert 'basic-package (1.1)' in news.changes
    assert 'basic-package (1.2)' in changelog.changes
    assert 'basic-package (1.1)' in changelog.changes


def test_doit(capsys):
    deb = find_test_file('chrome-gnome-shell_42.1-4_all.deb')
    config = make_config({'latest': 1})
    doit(config, ['apt-listchanges', deb])
    captured = capsys.readouterr()
    assert captured.out.endswith('Sat, 19 Aug 2023 07:49:01 -0400\n')


def test_dump_seen(capsys):
    config = make_config({'dump_seen': True})
    with pytest.raises(SystemExit):
        doit(config, ['apt-listchanges'])
    captured = capsys.readouterr()
    assert captured.out == ("Enabled debug output\npackages:\n"
                            "exact checksums:\nsimilar checksums:\n")


def test_doit_quiet():
    deb = find_test_file('chrome-gnome-shell_42.1-4_all.deb')
    config = make_config({'quiet': 1})
    doit(config, ['apt-listchanges', deb])


def test_unknown_frontend(capsys, mocker):
    mocker.patch('sys.stdout.isatty', lambda: True)
    deb = find_test_file('chrome-gnome-shell_42.1-4_all.deb')
    config = make_config({'frontend': 'nonsense'})
    with pytest.raises(SystemExit):
        doit(config, ['apt-listchanges', deb])
    captured = capsys.readouterr()
    assert _('Unknown frontend: %s') % 'nonsense' in captured.err


def test_frontend_none():
    deb = find_test_file('chrome-gnome-shell_42.1-4_all.deb')
    config = make_config({'quiet': 2})
    with pytest.raises(SystemExit):
        doit(config, ['apt-listchanges', deb])


def test_extract_filenames_via_installed():
    # We use ncurses-base for this test because it's marked Essential: yes so
    # it's guaranteed to be installed on any system running this test.
    pkg = package_from_deb_file('ncurses-base_6.4+20230625-2_all.deb')
    patterns = (pkg.news_filenames + pkg.changelog_filenames +
                pkg.binnmu_filenames)
    package_files = pkg._extract_filenames_via_installed(patterns)
    assert package_files
    print(package_files)


class AbortFrontend(Frontend):
    def confirm(self):
        return False


def test_redisplay_after_abort():
    with tempfile.NamedTemporaryFile(prefix='test_redisplay_after_abort') as f:
        os.unlink(f.name)
        deb = find_test_file('ncurses-base_6.4+20230625-2_all.deb')
        config = make_config(
            {'save_seen': f.name, 'latest': 1, 'confirm': True})
        config.frontend = AbortFrontend(config, 1)
        # This ugly force_db back is only necessary because we can't fully mock
        # doit because pytest-subprocess isn't supported in package tests (see
        # bug #1053360).
        with pytest.raises(SystemExit):
            doit(config, ['apt-listchanges', deb], force_db=True)
        seen_db = SeenDb(f.name)
        # If the package is in the database after the aborted run, then that
        # means the database was saved when it should haven't been, so that's
        # how we check to ensure the right thing happened.
        assert not seen_db.has_package('ncurses-base')
        config.frontend = Frontend(config, 1)
        doit(config, ['apt-listchanges', deb], force_db=True)
        seen_db = SeenDb(f.name)
        assert seen_db.has_package('ncurses-base')


def test_version_filtering(fs):
    fs.add_real_directory(os.path.dirname(__file__))
    install_yaml_package(fs, 'version_filtering.yaml')
    config = make_config()
    seen_db = SeenDb()
    status = make_dpkg_status(dpkg_status_file)
    notes = []
    filterer = Filterer(config, seen_db)
    pkg = package_from_yaml_file('version_filtering_1.2.yaml',
                                 filterer=filterer)
    (news, changelog) = process_pkg(config, seen_db, status, notes, pkg)
    print("CHANGELOG", changelog.changes)
    assert changelog.changes.startswith('basic-package (1.2-2)')
    assert changelog.changes.endswith('Sat, 30 Jan 2023 11:11:11 +0000\n')
