#!/usr/bin/env python3

# Unit tests for the Renderer abstraction.
# 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.

"""Unit tests for renderer.Dep5Renderer.

These pin each rendered piece in isolation; they must agree with the
generate_output characterization tests in output_test.py, which lock the
equivalent text at the (still-live) orchestration the renderer will replace.
"""

import contextlib
import io
import unittest

import debian.copyright as dc

from decopy import cmdoptions
from decopy.common.core_types import CopyrightHolder, LicenseRegistry
from decopy.dep5doc import dep5
from decopy.dep5doc.renderer import Dep5Renderer, Renderer, renderer_for
from decopy.fstree import nodes
from decopy.licensing.license_texts import LicenseTextResolver

_CURRENT_FORMAT = (
    "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n"
)


def _options(args):
    base = ["--no-progress", "--root", "tests/testdata/one"]
    return cmdoptions.process_options([*base, *args])


class HeaderTest(unittest.TestCase):
    def setUp(self):
        self.renderer = Dep5Renderer()

    def test_full_mode_without_copyright(self):
        header = self.renderer.header(None, _options([]))
        self.assertEqual(header, _CURRENT_FORMAT.rstrip("\n") + "\nSource: TODO")

    def test_partial_mode_lists_files(self):
        options = cmdoptions.process_options(["--mode=partial", "."])
        header = self.renderer.header(None, options)
        self.assertEqual(
            header,
            _CURRENT_FORMAT.rstrip("\n") + "\nSource: TODO\nComment: *** only: . ***",
        )

    def test_existing_copyright_overrides_format_keeps_fields(self):
        text = (
            "Format: https://example.org/old-format\n"
            "Upstream-Name: example\n"
            "Source: https://example.org/dl\n"
        )
        with contextlib.redirect_stderr(io.StringIO()):
            copyright_ = dep5.Copyright(dc.Copyright(io.StringIO(text), strict=False))
        header = self.renderer.header(copyright_, _options([]))
        self.assertEqual(
            header,
            _CURRENT_FORMAT.rstrip("\n")
            + "\nUpstream-Name: example\nSource: https://example.org/dl",
        )

    def test_header_does_not_mutate_the_copyright(self):
        """header() is a query: it must not write the canonical Format or a
        default Source back into the caller's document."""
        text = "Format: https://example.org/old-format\nUpstream-Name: example\n"
        with contextlib.redirect_stderr(io.StringIO()):
            copyright_ = dep5.Copyright(dc.Copyright(io.StringIO(text), strict=False))
        before_format = copyright_.header.format
        before_source = copyright_.header.source

        rendered = self.renderer.header(copyright_, _options([]))

        # The input is untouched ...
        self.assertEqual(copyright_.header.format, before_format)
        self.assertEqual(copyright_.header.source, before_source)
        # ... yet the rendered text still canonicalises Format and defaults
        # the missing Source to TODO.
        self.assertEqual(
            rendered,
            _CURRENT_FORMAT.rstrip("\n") + "\nUpstream-Name: example\nSource: TODO",
        )
        # A second render sees the same input and produces the same text.
        self.assertEqual(rendered, self.renderer.header(copyright_, _options([])))


class FileBlockTest(unittest.TestCase):
    def setUp(self):
        self.renderer = Dep5Renderer()

    def _group(self):
        root = nodes.DirInfo(parent=None, name="")
        group = dep5.Group(position=0)
        f = nodes.FileInfo(parent=root, name="README")
        f.add_licenses(["MIT"])
        f.add_copyrights([CopyrightHolder("Alice", "alice@x")])
        group.add_file(f)
        return group

    def test_file_block_renders_dep5_files_paragraph(self):
        options = _options(["--no-glob"])
        grp = self._group()
        block = self.renderer.file_block(grp, options, grp.files.sorted_members())
        self.assertEqual(
            block,
            "Files: README\nCopyright: Alice <alice@x>\nLicense: MIT",
        )

    def test_file_block_passes_through_extra_stored_fields(self):
        options = _options(["--no-glob"])
        grp = self._group()
        # The created paragraph already carries Files/Copyright/License; those
        # standard fields must NOT be echoed (the renderer emits them from the
        # group's own data), only the extra X-Foo passes through.
        stored = dc.FilesParagraph.create(
            ["README"], "Alice <alice@x>", dc.License("MIT")
        )
        stored["X-Foo"] = "bar"
        grp.stored = stored
        block = self.renderer.file_block(grp, options, grp.files.sorted_members())
        self.assertIn("X-Foo: bar", block)
        self.assertEqual(block.count("Files:"), 1)
        self.assertEqual(block.count("License:"), 1)
        self.assertEqual(block.count("Copyright:"), 1)


class LicenseBlockTest(unittest.TestCase):
    def setUp(self):
        self.registry = LicenseRegistry()
        self.renderer = Dep5Renderer()

    def test_no_stored_text_emits_placeholder(self):
        block = self.renderer.license_block(self.registry.get("MIT"), _options([]))
        self.assertEqual(
            block, "License: MIT\nComment: Add the corresponding license text here"
        )

    def test_stored_text_is_included(self):
        stored = dc.License(synopsis="MIT", text="Permission is granted ...")
        license_ = self.registry.get("MIT")
        license_.stored = dc.LicenseParagraph.create(stored)
        block = self.renderer.license_block(license_, _options([]))
        self.assertTrue(block.startswith("License: MIT"))
        self.assertIn("Permission is granted", block)

    def test_spdx_converts_display_name(self):
        block = self.renderer.license_block(
            self.registry.get("GPL-2"), _options(["--spdx"])
        )
        self.assertTrue(block.startswith("License: GPL-2.0-only\n"))

    def test_no_spdx_keeps_dep5_name(self):
        block = self.renderer.license_block(
            self.registry.get("GPL-2"), _options(["--no-spdx"])
        )
        self.assertTrue(block.startswith("License: GPL-2\n"))


class LicenseBlockFillTest(unittest.TestCase):
    def setUp(self):
        self.registry = LicenseRegistry()
        self.renderer = Dep5Renderer(LicenseTextResolver())

    def test_fills_common_licenses_reference(self):
        block = self.renderer.license_block(self.registry.get("GPL-2"), _options([]))
        self.assertTrue(block.startswith("License: GPL-2\n"))
        self.assertIn("/usr/share/common-licenses/GPL-2", block)
        self.assertNotIn("Add the corresponding license text here", block)

    def test_uncurated_name_keeps_placeholder(self):
        block = self.renderer.license_block(self.registry.get("Beerware"), _options([]))
        self.assertEqual(
            block,
            "License: Beerware\nComment: Add the corresponding license text here",
        )

    def test_reference_lines_within_78_columns(self):
        block = self.renderer.license_block(self.registry.get("GPL-2"), _options([]))
        for line in block.split("\n"):
            self.assertLessEqual(len(line), 78, line)

    def test_stored_text_wins_over_resolver(self):
        license_ = self.registry.get("GPL-2")
        stored = dc.License(synopsis="GPL-2", text="STORED-BODY-MARKER")
        license_.stored = dc.LicenseParagraph.create(stored)
        block = self.renderer.license_block(license_, _options([]))
        self.assertIn("STORED-BODY-MARKER", block)
        self.assertNotIn("/usr/share/common-licenses", block)

    def test_resolver_keys_on_dep5_name_under_spdx(self):
        block = self.renderer.license_block(
            self.registry.get("GPL-2"), _options(["--spdx"])
        )
        # Display name is SPDX, but the body is resolved from the DEP-5 key.
        self.assertTrue(block.startswith("License: GPL-2.0-only\n"))
        self.assertIn("/usr/share/common-licenses/GPL-2", block)

    def test_no_resolver_keeps_placeholder(self):
        block = Dep5Renderer().license_block(self.registry.get("GPL-2"), _options([]))
        self.assertEqual(
            block, "License: GPL-2\nComment: Add the corresponding license text here"
        )


class ProtocolConformanceTest(unittest.TestCase):
    def test_dep5renderer_is_a_renderer(self):
        self.assertIsInstance(Dep5Renderer(), Renderer)


class RendererForTest(unittest.TestCase):
    def test_selects_dep5_renderer(self):
        self.assertIsInstance(renderer_for(_options([])), Dep5Renderer)

    def test_returns_a_renderer(self):
        self.assertIsInstance(renderer_for(_options([])), Renderer)

    def test_fill_on_fills_reference(self):
        options = _options([])
        block = renderer_for(options).license_block(
            LicenseRegistry().get("GPL-2"), options
        )
        self.assertIn("/usr/share/common-licenses/GPL-2", block)

    def test_fill_off_keeps_placeholder(self):
        options = _options(["--no-fill-licenses"])
        block = renderer_for(options).license_block(
            LicenseRegistry().get("GPL-2"), options
        )
        self.assertIn("Add the corresponding license text here", block)


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