# derivative-dist-compare, Debian package tracker for derivative distributions
# Copyright (C) 2024-2026 IKUS Software <patrik@ikus-soft.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import shutil
import tempfile
import unittest
from pathlib import Path

import yaml
from parameterized import parameterized

from derivative_dist_compare.main import (
    ArchInfo,
    BinaryPkg,
    Comparator,
    Config,
    DistInfo,
    PackageSet,
    PkgInfo,
    SourceInfo,
    SourcePkg,
    Status,
    compare_version,
    main,
)

CFG_DATA = """
archs:
  - amd64
  - arm64

cache-dir: /tmp

distros:
  debian-buster:
    sources.list: |
        deb http://deb.debian.org/debian buster main
        deb http://deb.debian.org/debian-security buster/updates main

  ubuntu-focal:
    parent: debian-buster
    sources.list: |
        deb http://archive.ubuntu.com/ubuntu focal main
        deb http://archive.ubuntu.com/ubuntu focal-updates main

package-sets:
  linux:
    - cryptsetup
    - ethtool
    - initramfs-tools
    - libnvme
    - linux
    - linux-base
    - nvme-cli
    - raspi-firmware
  nerdlog:
    - nerdlog:
      - golang-github-dimonomid-clock
      - golang-github-dimonomid-ssh-config
      - golang-golang-x-clipboard:
        - golang-golang-x-mobile
"""

TEST1_PARENT_PATH = Path(__file__, "../data/test1-parent").resolve()
TEST1_DERIVATIVE_PATH = Path(__file__, "../data/test1-derivative").resolve()

TEST2_PARENT_PATH = Path(__file__, "../data/test2-parent").resolve()
TEST2_DERIVATIVE_PATH = Path(__file__, "../data/test2-derivative").resolve()

TEST3_PARENT_PATH = Path(__file__, "../data/test3-parent").resolve()
TEST3_DERIVATIVE_PATH = Path(__file__, "../data/test3-derivative").resolve()

TEST4_PARENT_PATH = Path(__file__, "../data/test4-parent").resolve()
TEST4_DERIVATIVE_PATH = Path(__file__, "../data/test4-derivative").resolve()


class TestConfig(unittest.TestCase):

    def test_config(self):
        # Given a valid configuration file
        with tempfile.NamedTemporaryFile(mode='w+', prefix='derivative-dist-compare-config', suffix='.yml') as f:
            f.write(CFG_DATA)
            f.flush()
            # When reading the configuration file
            config = Config(f.name)
            # Then configuration is valid
            self.assertEqual(config.get_archs(), ['amd64', 'arm64'])
            self.assertEqual(config.get_cache_dir(), Path('/tmp'))
            self.assertEqual(config.all_dist(), ['debian-buster', 'ubuntu-focal'])
            self.assertEqual(
                config.dist_pairs(), [{'dist': 'ubuntu-focal', 'parent': 'debian-buster', 'vendor_suffix': False}]
            )
            self.assertIn('http://archive.ubuntu.com/ubuntu', config.get_sources('ubuntu-focal'))
            self.assertEqual(
                config.get_package_sets(),
                [
                    ('cryptsetup', PackageSet(name='linux', depth=0, order=0)),
                    ('ethtool', PackageSet(name='linux', depth=0, order=1)),
                    ('initramfs-tools', PackageSet(name='linux', depth=0, order=2)),
                    ('libnvme', PackageSet(name='linux', depth=0, order=3)),
                    ('linux', PackageSet(name='linux', depth=0, order=4)),
                    ('linux-base', PackageSet(name='linux', depth=0, order=5)),
                    ('nvme-cli', PackageSet(name='linux', depth=0, order=6)),
                    ('raspi-firmware', PackageSet(name='linux', depth=0, order=7)),
                    ('nerdlog', PackageSet(name='nerdlog', depth=0, order=0)),
                    ('golang-github-dimonomid-clock', PackageSet(name='nerdlog', depth=1, order=1)),
                    ('golang-github-dimonomid-ssh-config', PackageSet(name='nerdlog', depth=1, order=2)),
                    ('golang-golang-x-clipboard', PackageSet(name='nerdlog', depth=1, order=3)),
                    ('golang-golang-x-mobile', PackageSet(name='nerdlog', depth=2, order=4)),
                ],
            )


class TestComparator(unittest.TestCase):

    _temp_paths = []

    def tearDown(self):
        # Delete temp directories.
        for path in self._temp_paths:
            shutil.rmtree(path, ignore_errors=True)
        return super().tearDown()

    def _mkdtemp(self):
        """
        Create a temporary directory for the test.
        """
        path = tempfile.mkdtemp(prefix='derivative-dist-compare-test')
        self._temp_paths.append(path)
        return path

    def test_create_pkg_index(self):
        # Given two repo
        cfg = Config(
            {
                'cache-dir': self._mkdtemp(),
                'archs': ['amd64', 'arm64'],
                'distros': {
                    'repo': {'sources': f"deb copy:{TEST1_PARENT_PATH} /\n"},
                    'repo2': {'sources': f"deb copy:{TEST1_DERIVATIVE_PATH} /\n"},
                },
            }
        )
        # When creating package index
        comparator = Comparator(cfg)
        comparator.create_pkg_index('repo')
        comparator.create_pkg_index('repo2')
        # Then binary packages get populated
        self.assertEqual(
            comparator.binary_pkgs,
            [
                BinaryPkg(
                    dist='repo',
                    name='abrowser',
                    arch='all',
                    version='3.6.9+build1',
                    source_name='firefox',
                    source_ver='3.6.9+build1',
                    udeb=False,
                ),
                BinaryPkg(
                    dist='repo2',
                    name='abrowser',
                    arch='all',
                    version='3.6.9+build1+nobinonly-0ubuntu1',
                    source_name='firefox',
                    source_ver='3.6.9+build1+nobinonly-0ubuntu1',
                    udeb=False,
                ),
            ],
        )

    def test_compare(self):
        # Given two repo
        cfg = Config(
            {
                'cache-dir': self._mkdtemp(),
                'archs': ['amd64', 'arm64'],
                'distros': {
                    'repo': {'sources': f"deb copy:{TEST1_PARENT_PATH} /\n"},
                    'repo2': {
                        'sources': f"deb copy:{TEST1_DERIVATIVE_PATH} /\n",
                        'parent': 'repo',
                        'vendor-suffix': 'ubuntu',
                    },
                },
            }
        )
        # When creating package index
        comparator = Comparator(cfg)
        data = comparator.compare()
        # Then comparaison return greater_version
        self.assertEqual(
            data,
            [
                SourceInfo(
                    source_name='firefox',
                    homepage=None,
                    vcs_browser=None,
                    package_set=None,
                    dist={
                        'repo2': DistInfo(
                            source_ver='3.6.9+build1+nobinonly-0ubuntu1',
                            source_ver_parent='3.6.9+build1',
                            compare=Status.greater_version,
                            arch={},
                            vcs_browser=None,
                        )
                    },
                    duplicate=False,
                    tracker_url=None,
                    bugs_url=None,
                    download=None,
                )
            ],
        )

    def test_compare_arch_missing(self):
        # Given two repos
        # Parent:
        #   sid rustc all 1.86.0+dfsg1-1~exp4
        #   sid rustc amd64 1.86.0+dfsg1-1~exp4
        #   sid rustc arm64 1.86.0+dfsg1-1~exp4
        # Derivative:
        #   trixie-fastforward-backports rustc all 1.86.0+dfsg1-1~exp4~ffwd13+u1
        #   trixie-fastforward-backports rustc amd64 1.86.0+dfsg1-1~exp4~ffwd13+u1
        #   **trixie-fastforward-backports rustc arm64 missing**
        cfg = Config(
            {
                'cache-dir': self._mkdtemp(),
                'archs': ['amd64', 'arm64'],
                'distros': {
                    'sid': {'sources': f"deb copy:{TEST2_PARENT_PATH} sid main\n"},
                    'trixie-fastforward-backports': {
                        'sources': f"deb copy:{TEST2_DERIVATIVE_PATH} trixie-fastforward-backports main\n",
                        'parent': 'sid',
                        'vendor-suffix': 'ffwd',
                    },
                },
            }
        )
        # When creating package index
        comparator = Comparator(cfg)
        data = comparator.compare()
        # Then comparaison is uptodate, but specific architecture is not_provided
        self.assertEqual(
            data,
            [
                SourceInfo(
                    source_name='rustc',
                    homepage=None,
                    vcs_browser=None,
                    package_set=None,
                    dist={
                        'trixie-fastforward-backports': DistInfo(
                            source_ver='1.86.0+dfsg1-1~ffwd13+u1',
                            source_ver_parent='1.86.0+dfsg1-1',
                            compare=Status.uptodate,
                            arch={
                                'arm64': ArchInfo(
                                    compare=Status.not_provided,
                                    packages=[
                                        PkgInfo(
                                            name='rustc',
                                            version=None,
                                            version_parent='1.86.0+dfsg1-1',
                                            compare=Status.not_provided,
                                        )
                                    ],
                                )
                            },
                            vcs_browser=None,
                        )
                    },
                )
            ],
        )

    def test_compare_arch_outdated(self):
        # Given two repos
        # Parent:
        #   sid rustc all 1.86.0+dfsg1-1~exp4
        #   sid rustc amd64 1.86.0+dfsg1-1~exp4
        #   sid rustc arm64 1.86.0+dfsg1-1~exp4
        # Derivative:
        #   trixie-fastforward-backports rustc all 1.86.0+dfsg1-1~exp4~ffwd13+u1
        #   trixie-fastforward-backports rustc amd64 1.86.0+dfsg1-1~exp4~ffwd13+u1
        #   **trixie-fastforward-backports rustc arm64 1.85.0+dfsg1-1~exp4~ffwd13+u1**
        cfg = Config(
            {
                'cache-dir': self._mkdtemp(),
                'archs': ['amd64', 'arm64'],
                'distros': {
                    'sid': {'sources': f"deb copy:{TEST3_PARENT_PATH} sid main\n"},
                    'trixie-fastforward-backports': {
                        'sources': f"deb copy:{TEST3_DERIVATIVE_PATH} trixie-fastforward-backports main\n",
                        'parent': 'sid',
                        'vendor-suffix': 'ffwd',
                    },
                },
            }
        )
        # When creating package index
        comparator = Comparator(cfg)
        data = comparator.compare()
        # Then comparaison is uptodate, but specific architecture is lesser_version
        self.assertEqual(
            data,
            [
                SourceInfo(
                    source_name='rustc',
                    homepage=None,
                    vcs_browser=None,
                    package_set=None,
                    dist={
                        'trixie-fastforward-backports': DistInfo(
                            source_ver='1.86.0+dfsg1-1~ffwd13+u1',
                            source_ver_parent='1.86.0+dfsg1-1',
                            compare=Status.uptodate,
                            arch={
                                'arm64': ArchInfo(
                                    compare=Status.lesser_version,
                                    packages=[
                                        PkgInfo(
                                            name='rustc',
                                            version='1.85.0+dfsg1-1~ffwd13+u1',
                                            version_parent='1.86.0+dfsg1-1',
                                            compare=Status.lesser_version,
                                        )
                                    ],
                                )
                            },
                            vcs_browser=None,
                        )
                    },
                )
            ],
        )

    def test_compare_with_sources(self):
        # Given two repos with sources
        cfg = Config(
            {
                'cache-dir': self._mkdtemp(),
                'archs': ['amd64', 'arm64'],
                'distros': {
                    'sid': {
                        'sources': f"deb copy:{TEST3_PARENT_PATH} sid main\ndeb-src copy:{TEST3_PARENT_PATH} sid main\n"
                    },
                    'trixie-fastforward-backports': {
                        'sources': f"deb copy:{TEST3_DERIVATIVE_PATH} trixie-fastforward-backports main\ndeb-src copy:{TEST3_DERIVATIVE_PATH} trixie-fastforward-backports main\n",
                        'parent': 'sid',
                        'vendor-suffix': 'ffwd',
                    },
                },
            }
        )
        # When creating package index
        comparator = Comparator(cfg)
        data = comparator.compare()
        # Then sources packages get populated
        self.assertEqual(
            comparator.source_pkgs,
            [
                SourcePkg(
                    dist='sid',
                    source_name='rustc',
                    source_ver='1.86.0+dfsg2-3',
                    homepage='http://www.rust-lang.org/',
                    vcs_browser='https://salsa.debian.org/rust-team/rust',
                    download=None,
                ),
                SourcePkg(
                    dist='trixie-fastforward-backports',
                    source_name='rustc',
                    source_ver='1.86.0+dfsg1-1~exp1~ffwd13+u1',
                    homepage='http://www.rust-lang.org/',
                    vcs_browser='https://git.fastforward.debian.net/trixie-fastforward-backports/rustc',
                    download=None,
                ),
            ],
        )
        # Then comparaison include details information like homepage and vcs_browser url.
        self.assertEqual(
            data,
            [
                SourceInfo(
                    source_name='rustc',
                    homepage='http://www.rust-lang.org/',
                    vcs_browser='https://salsa.debian.org/rust-team/rust',
                    package_set=None,
                    dist={
                        'trixie-fastforward-backports': DistInfo(
                            source_ver='1.86.0+dfsg1-1~ffwd13+u1',
                            source_ver_parent='1.86.0+dfsg1-1',
                            compare=Status.uptodate,
                            arch={
                                'arm64': ArchInfo(
                                    compare=Status.lesser_version,
                                    packages=[
                                        PkgInfo(
                                            name='rustc',
                                            version='1.85.0+dfsg1-1~ffwd13+u1',
                                            version_parent='1.86.0+dfsg1-1',
                                            compare=Status.lesser_version,
                                        )
                                    ],
                                )
                            },
                            vcs_browser='https://git.fastforward.debian.net/trixie-fastforward-backports/rustc',
                        )
                    },
                )
            ],
        )

    @parameterized.expand(
        [
            ('20230311-0.0~progress6.99u1', '20230311', 'progress', Status.uptodate),
            ('23-1progress6u1', '23-1', 'progress', Status.uptodate),
            ('225+deb11u1-0progress6u1', '225+deb11u1', 'progress', Status.uptodate),
            ('13.11.4-0.0~progress6.99u1', '13.11.4', 'progress', Status.uptodate),
            ('12.4+deb12u5-0.0~progress6.99u1', '12.4+deb12u7', 'progress', Status.lesser_version),
            ('1.2.3-4ffwd12u1', '1.2.3-4', 'ffwd', Status.uptodate),
            ('1.2.3ffwd12u1', '1.2.3', 'ffwd', Status.uptodate),
            ('1.2.3-0ffwd12u1', '1.2.3', 'ffwd', Status.uptodate),
            ('1.2.3-4+deb12u1ffwd12u1', '1.2.3-4+deb12u1', 'ffwd', Status.uptodate),
            ('1.2.3+deb12u1ffwd12u1', '1.2.3+deb12u1', 'ffwd', Status.uptodate),
            ('1.2.3+deb12u1-0ffwd12u1', '1.2.3+deb12u1', 'ffwd', Status.uptodate),
            ('1.2.3-4~deb12u1ffwd12u1', '1.2.3-4~deb12u1', 'ffwd', Status.uptodate),
            ('1.2.3~deb12u1ffwd12u1', '1.2.3~deb12u1', 'ffwd', Status.uptodate),
            ('1.2.3~deb12u1-0ffwd12u1', '1.2.3~deb12u1', 'ffwd', Status.uptodate),
            ('1.2.3-4~ffwd12+u1', '1.2.3-4', 'ffwd', Status.uptodate),
            ('1.2.3-4+ffwd12+u1', '1.2.3-4', 'ffwd', Status.uptodate),
            ('1.2.3ffwd12+u1', '1.2.3', 'ffwd', Status.uptodate),
            ('1.2.3-0.0~ffwd12+u1', '1.2.3', 'ffwd', Status.uptodate),
            ('1.2.3~ffwd12+u1', '1.2.3', 'ffwd', Status.uptodate),
            ('1.2.3+ffwd12+u1', '1.2.3', 'ffwd', Status.uptodate),
            ('1.2.3-4+b1ffwd12u1', '1.2.3-4+b1', 'ffwd', Status.uptodate),
            ('1.2.3+b1ffwd12u1', '1.2.3+b1', 'ffwd', Status.uptodate),
            ('1.2.3-4.1ffwd12u1', '1.2.3-4.1', 'ffwd', Status.uptodate),
            # Test bin NMU
            ('1.2.3+nmu1ffwd12u1', '1.2.3+nmu1', 'ffwd', Status.uptodate),
            ('5.2.37-2+b4progress8u1', '5.2.37-2', 'progress', Status.uptodate, True),
            ('5.2.37-2+b4progress8u1', '5.2.37-2', 'progress', Status.greater_revision, False),
            # Test lesser revision
            ('1.2.3-1ffwd', '1.2.3-2', 'ffwd', Status.lesser_revision),
            (False, '1.2.3', 'progress', Status.not_provided),
            ('1.2.3', False, 'progress', Status.not_found),
        ]
    )
    def test_compare_version(self, derivative_version, parent_version, vendor_suffix, expected_status, source=False):
        self.assertEqual(
            compare_version(derivative_version, parent_version, vendor_suffix, source=source), expected_status
        )

    def test_main_e2e(self):
        with tempfile.NamedTemporaryFile(mode='w+', prefix='derivative-dist-compare-config', suffix='.yml') as f:
            # Given a configuration file.
            yaml.dump(
                {
                    'cache-dir': self._mkdtemp(),
                    'archs': ['amd64', 'arm64'],
                    'distros': {
                        'sid': {
                            'sources': f"deb copy:{TEST4_PARENT_PATH} sid main\ndeb-src copy:{TEST4_PARENT_PATH} sid main\n",
                            'bugs-url': 'https://bugs.debian.org/cgi-bin/pkgreport.cgi?src=%s&repeatmerged=no',
                            'tracker-url': 'https://tracker.debian.org/pkg/',
                        },
                        'trixie-fastforward-backports': {
                            'sources': f"deb copy:{TEST4_DERIVATIVE_PATH} trixie-fastforward-backports main\ndeb-src copy:{TEST4_DERIVATIVE_PATH} trixie-fastforward-backports main\n",
                            'parent': 'sid',
                            'vendor-suffix': 'ffwd',
                        },
                    },
                    'package-sets': {'firefox': [{'firefox': ['nss']}], 'vendor': ['supermicro-ipmicfg-amd64']},
                },
                stream=f,
            )
            f.flush()
            # When calling main entry point
            temp_dir = self._mkdtemp()
            json_output = Path(temp_dir) / "output.json"
            html_output = Path(temp_dir) / "output.html"
            main(["-f", f.name, "--json-output", str(json_output), "--html-output", str(html_output)])
            # Then file get created.
            self.assertTrue(json_output.is_file())
            self.assertTrue(html_output.is_file())
            # Then our html contains bugs-url
            html_data = html_output.read_text()
            self.assertIn(
                'href="https://bugs.debian.org/cgi-bin/pkgreport.cgi?src=rustc&amp;repeatmerged=no"', html_data
            )
            # Then our html contains tracker-url
            self.assertIn('href="https://tracker.debian.org/pkg/rustc"', html_data)
            # Then our html include download url for supermicro-ipmicfg-amd64
            self.assertIn('href="https://www.supermicro.com/wdl/utility/IPMICFG/"', html_data)
            # Then our html does not include url to download rustc.
            self.assertNotIn('href="https://forge.rust-lang.org/infra/other-installation-methods.html"', html_data)
