#!/usr/bin/env python3
# Copyright: 2026, Maximiliano Curia <maxy@debian.org>
#
# License: ISC
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#  .
#  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
#  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
#  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
#  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
#  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
#  PERFORMANCE OF THIS SOFTWARE.

"""Tests for cmdoptions CLI flags."""

import dataclasses
import tempfile
import typing
import unittest
from pathlib import Path

from decopy import cmdoptions
from decopy.common.core_types import Options

# The Options-owned defaults the CLI must reuse rather than re-declare.
_SHARED_DEFAULT_FIELDS = (
    "spdx",
    "explain",
    "since",
    "expand_archives",
    "max_archive_depth",
    "max_archive_member_size",
    "use_gitignore",
    "fill_licenses",
)


def _option_default(name):
    field = next(f for f in dataclasses.fields(Options) if f.name == name)
    return field.default


class FillLicensesOptionTest(unittest.TestCase):
    """Tests for the --fill-licenses / --no-fill-licenses option."""

    _BASE: typing.ClassVar[list[str]] = ["--no-progress", "--root", "."]

    def test_defaults_to_true(self):
        opts = cmdoptions.process_options(self._BASE)
        self.assertTrue(opts.fill_licenses)

    def test_no_flag_disables(self):
        opts = cmdoptions.process_options([*self._BASE, "--no-fill-licenses"])
        self.assertFalse(opts.fill_licenses)

    def test_flag_enables(self):
        opts = cmdoptions.process_options([*self._BASE, "--fill-licenses"])
        self.assertTrue(opts.fill_licenses)

    def test_last_flag_wins(self):
        opts = cmdoptions.process_options(
            [*self._BASE, "--fill-licenses", "--no-fill-licenses"]
        )
        self.assertFalse(opts.fill_licenses)

    def test_field_is_bool(self):
        opts = cmdoptions.process_options(self._BASE)
        self.assertIsInstance(opts, Options)
        self.assertIs(type(opts.fill_licenses), bool)


class TestProcessOptionsDefaults(unittest.TestCase):
    """The CLI defaults come from the Options dataclass (single source)."""

    def test_defaults_match_options_dataclass(self):
        with tempfile.TemporaryDirectory() as tmp:
            options = cmdoptions.process_options(["--no-progress", "--root", tmp])
        for name in _SHARED_DEFAULT_FIELDS:
            self.assertEqual(
                getattr(options, name),
                _option_default(name),
                f"{name}: CLI default diverged from Options default",
            )


class TestProcessOptionsAssembly(unittest.TestCase):
    """Characterisation snapshot: a representative argv -> Options field values.

    Pins the parse/assemble behaviour so the process_options decomposition
    cannot change any produced value.
    """

    def test_flag_matrix_produces_expected_options(self):
        with tempfile.TemporaryDirectory() as tmp:
            options = cmdoptions.process_options(
                [
                    "--no-progress",
                    "--spdx",
                    "--no-glob",
                    "--group-by",
                    "copyright",
                    "--mode",
                    "partial",
                    "--jobs",
                    "4",
                    "--since",
                    "HEAD~1",
                    "--max-archive-depth",
                    "3",
                    "--no-fill-licenses",
                    "--copyright-file",
                    "debian/copyright",
                    "--output",
                    "-",
                    "--root",
                    tmp,
                ]
            )

        self.assertEqual(options.mode, "partial")
        self.assertEqual(options.group_by, "copyright")
        self.assertEqual(options.jobs, 4)
        self.assertEqual(options.since, "HEAD~1")
        self.assertEqual(options.max_archive_depth, 3)
        self.assertEqual(options.copyright_file, "debian/copyright")
        self.assertTrue(options.spdx)
        self.assertFalse(options.glob)
        self.assertFalse(options.progress)
        self.assertFalse(options.fill_licenses)
        # "-" is the stdout sentinel, normalised to the empty string.
        self.assertEqual(options.output, "")
        # root is absolutised.
        self.assertEqual(options.root, str(Path(tmp).absolute()))
        # The four exclude regexes are compiled onto the result.
        for name in (
            "exclude_fullname_re",
            "exclude_special_re",
            "exclude_file_re",
            "exclude_directory_re",
        ):
            self.assertIsNotNone(getattr(options, name))

    def test_changed_shorthand_sets_mode(self):
        with tempfile.TemporaryDirectory() as tmp:
            options = cmdoptions.process_options(
                ["--no-progress", "--changed", "--root", tmp]
            )
        self.assertEqual(options.mode, "changed")

    def test_files_are_relative_to_root(self):
        with tempfile.TemporaryDirectory() as tmp:
            (Path(tmp) / "a.c").write_text("", encoding="utf-8")
            options = cmdoptions.process_options(
                ["--no-progress", "--root", tmp, str(Path(tmp) / "a.c")]
            )
        self.assertEqual(options.files, ["a.c"])

    def test_every_required_option_field_is_produced(self):
        """A required Options field with no CLI wiring would fail construction.

        Guards the namespace->Options assembly: a new required field on Options
        without a matching argument makes process_options raise.
        """
        with tempfile.TemporaryDirectory() as tmp:
            options = cmdoptions.process_options(["--no-progress", "--root", tmp])
        required = [
            f.name
            for f in dataclasses.fields(Options)
            if f.default is dataclasses.MISSING
            and f.default_factory is dataclasses.MISSING
        ]
        for name in required:
            self.assertTrue(hasattr(options, name))


if __name__ == "__main__":
    unittest.main()
