#!/usr/bin/env python3
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import sys
import unittest
from typing import Any, Callable

_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
# The repo's root directory.
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", ".."))

# Add the repo's root directory for clearer imports.
sys.path.insert(0, _ROOT_DIR)

from metadata.fields.field_types import MetadataField  # noqa: E402
import metadata.fields.known as fields  # noqa: E402
from metadata.dependency_metadata import DependencyMetadata  # noqa: E402
import metadata.fields.custom.update_mechanism  # noqa: E402


class FieldValidationTest(unittest.TestCase):
    """Tests narrow_type() on fields we validate and extract structural data."""

    def _test_on_field(self, field: MetadataField) -> Callable:

        def expect(value: str, expected_value: Any, reason: str):
            output = field.narrow_type(value)
            self.assertEqual(
                output,
                expected_value,
                f'Field "{field.get_name()}" should {reason}. Input value'
                f' was: "{value}", but got coerced into {repr(output)}',
            )

        return expect

    def test_name(self):
        expect = self._test_on_field(fields.NAME)
        expect("package name", "package name", "return as-is")
        expect("", "", "not coerce empty string to `None`")

    def test_short_name(self):
        expect = self._test_on_field(fields.SHORT_NAME)
        expect("pkg-name", "pkg-name", "return as-is")
        expect("", "", "not coerce empty string to `None`")

    def test_url(self):
        expect = self._test_on_field(fields.URL)
        expect("", None, "treat empty string as None")
        expect(
            "https://example.com/", ["https://example.com/"], "return valid url"
        )
        expect(
            "https://example.com/,\nhttps://example2.com/",
            ["https://example.com/", "https://example2.com/"],
            "return multiple valid urls",
        )
        expect("file://test", [], "reject unsupported scheme")
        expect(
            "file://test,\nhttps://example.com",
            ["https://example.com"],
            "reject unsupported scheme",
        )
        expect(
            "HTTPS://example.com", ["https://example.com"], "canonicalize url"
        )
        expect("http", [], "reject invalid url")
        expect(
            "This is the canonical repo.",
            None,
            "understand the this repo is canonical message",
        )

    def test_version(self):
        expect = self._test_on_field(fields.VERSION)
        expect("", None, "treat empty string as None")
        expect("0", None, "treat invalid value as None")
        expect("varies", None, "treat invalid value as None")
        expect("see deps", None, "treat invalid value as None")
        expect("N/A", None, "N/A is treated as None")
        expect("Not applicable.", None, "N/A is treated as None")

    def test_date(self):
        expect = self._test_on_field(fields.DATE)
        expect("", None, "treat empty string as None")
        expect("0", None, "treat invalid value as None")
        expect("varies", None, "treat invalid value as None")
        expect("2024-01-02", "2024-01-02", "accepts ISO 8601 date")
        expect(
            "2024-01-02T03:04:05Z", "2024-01-02", "accepts ISO 8601 date time"
        )
        expect("Jan 2 2024", "2024-01-02", "accepts locale format")
        expect(
            "02/03/2000",
            "2000-03-02",
            "accepts ambiguous MM/DD format (better than no date info at all)",
        )
        expect("11/30/2000", "2000-11-30", "accepts unambiguous MM/DD format")

    def test_revision(self):
        expect = self._test_on_field(fields.REVISION)
        expect("", None, "treat empty string as None")
        expect("0", None, "treat invalid value as None")
        expect("varies", None, "treat invalid value as None")
        expect("see deps", None, "treat invalid value as None")
        expect("N/A", None, "N/A is treated as None")
        expect("Not applicable.", None, "N/A is treated as None")
        expect("invalid", None, "treat invalid hex as None")
        expect("123456", None, "treat too short hex as None")
        expect(
            "0123456789abcdef0123456789abcdef01234567abcabc",
            "0123456789abcdef0123456789abcdef01234567abcabc",
            "leave an abbreviated SHA-256 (41-63 chars) unchanged",
        )
        expect(
            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01",
            None,
            "treat hex longer than SHA-256 (>64) as None",
        )
        expect("varies", None, "treat varies as None")
        expect("see deps", None, "treat see deps as None")
        expect("N/A", None, "treat N/A as None")
        expect("Not applicable.", None, "treat 'Not applicable.' as None")
        expect("Head", None, "treat 'Head' as None")
        expect("abcdef1", "abcdef1", "leave valid hex unchanged")
        expect(
            "abcdef1abcdef1",
            "abcdef1abcdef1",
            "leave valid hex unchanged if between 7-64 chars",
        )
        expect(
            "0123456789abcdef0123456789abcdef01234567",
            "0123456789abcdef0123456789abcdef01234567",
            "leave a full SHA-1 (40 chars) unchanged",
        )
        expect(
            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
            "leave a full SHA-256 (64 chars) unchanged",
        )

    def test_license(self):
        expect = self._test_on_field(fields.LICENSE)
        expect("", None, "treat empty string as None")
        expect("LICENSE-1", ["LICENSE-1"], "return as a list")
        expect("LGPL v2, BSD", ["LGPL v2", "BSD"], "return as a list")

    def test_license_file(self):
        # TODO(b/321154076): Consider excluding files that doesn't exist on
        # disk if it's not too hard.
        #
        # Right now, we return the unparsed license file field as-is.
        expect = self._test_on_field(fields.LICENSE_FILE)
        expect("src/file", "src/file", "return value as-is")

    def test_security_critical(self):
        expect = self._test_on_field(fields.SECURITY_CRITICAL)
        expect("yes", True, "understand truthy value")
        expect("Yes", True, "understand truthy value")
        expect("no", False, "understand falsey value")
        expect(
            "No, because", False, "understand falsey value, with description"
        )

    def test_shipped(self):
        expect = self._test_on_field(fields.SHIPPED)
        expect("yes", True, "understand truthy value")
        expect("Yes, but", True, "understand truthy value with extra comment")
        expect("no", False, "understand falsey value")
        expect(
            "no, because", False, "understand falsey value, with extra comment"
        )

    def test_shipped_in_chromium(self):
        expect = self._test_on_field(fields.SHIPPED_IN_CHROMIUM)
        expect("yes", True, "understand truthy value")
        expect("Yes", True, "understand truthy value")
        expect("no", False, "understand falsey value")
        expect(
            "no, because", False, "understand falsey value, with extra comment"
        )

    def test_license_android_compatible(self):
        expect = self._test_on_field(fields.LICENSE_ANDROID_COMPATIBLE)
        expect("yes", True, "understand truthy value")
        expect("Yes", True, "understand truthy value")
        expect("no", False, "understand falsey value")
        expect(
            "no, because", False, "understand falsey value, with extra comment"
        )

    def test_cpe_prefix(self):
        expect = self._test_on_field(fields.CPE_PREFIX)
        expect("unknown", None, "treat unknown as None")
        expect("Unknown", None, "treat unknown as None")
        expect("bad_cpe_format", None, "rejects invalid value")
        expect("cpe:/a:d3", "cpe:/a:d3", "accept a valid cpe prefix")
        expect("cpe:/a:D3", "cpe:/a:d3", "normalize to lowercase")

    def test_description(self):
        expect = self._test_on_field(fields.DESCRIPTION)
        expect("desc", "desc", "return value as-is")

    def test_local_modification(self):
        expect = self._test_on_field(fields.LOCAL_MODIFICATIONS)
        expect("none", False, "understands none")
        expect("(none)", False, "understands none")
        expect("not applicable", False, "understands N/A")
        expect("", False, "treat empty string as False")
        expect(
            "modified X file",
            "modified X file",
            "return value as-is if it doesn't mean no modification",
        )

    def test_dependency_data_return_as_property(self):
        dm = DependencyMetadata()
        dm.add_entry("name", "package")
        dm.add_entry("url", "git://git@example.com,\nbad_url://example.com")
        dm.add_entry("security critical", "no")
        dm.add_entry("date", "2024-01-02")
        dm.add_entry("revision", "")

        self.assertEqual(dm.name, "package")
        self.assertEqual(dm.url, ["git://git@example.com"])
        self.assertEqual(dm.security_critical, False)
        self.assertEqual(dm.date, "2024-01-02")
        self.assertEqual(dm.revision, None)
        self.assertEqual(dm.version, None)

    def test_dependency_data_repo_is_canonical(self):
        dm = DependencyMetadata()
        dm.add_entry("name", "package")
        dm.add_entry("url", "This is the canonical repo.")

        self.assertEqual(dm.url, None)
        self.assertEqual(dm.is_canonical, True)

    def test_update_mechanism(self):
        """Tests type narrowing for the Update Mechanism field."""
        expect = self._test_on_field(
            metadata.fields.custom.update_mechanism.UpdateMechanismField()
        )

        # Test cases that should successfully parse.
        expect(
            "Autoroll",
            ("Autoroll", None, None),
            "parse a valid value with no bug",
        )
        expect(
            "  Autoroll  ",
            ("Autoroll", None, None),
            "parse a valid value and strip whitespace",
        )
        expect(
            "Manual (https://crbug.com/12345)",
            ("Manual", None, "https://crbug.com/12345"),
            "parse a valid value with https://crbug.com/ format",
        )
        expect(
            "Manual",
            ("Manual", None, None),
            "allow Manual with no bug (should still warn)",
        )
        expect(
            "Static.HardFork (https://crbug.com/12345)",
            ("Static", "HardFork", "https://crbug.com/12345"),
            "parse a namespaced value with a bug",
        )

        # Test cases that are invalid and should not be parsed.
        expect(
            "Static", (None, None, None), "Static with no bug to be an error"
        )
        expect("", (None, None, None), "treat empty string as None")
        expect("  ", (None, None, None), "treat whitespace-only string as None")
        expect(
            "Invalid",
            (None, None, None),
            "treat a syntactically invalid value as None",
        )
        expect(
            "Custom (some-bug)",
            (None, None, None),
            "treat an unknown mechanism as None",
        )
        expect(
            "Static.HardFork (A bug link)",
            (None, None, None),
            "parse a namespaced value with an invalid comment",
        )
        expect(
            "Manual (b/12345)", (None, None, None), "not parse a bug with b/"
        )
        expect(
            "Autoroll (crbug.com/12345)",
            (None, None, None),
            "not allow autoroll with a bug",
        )
        expect(
            "Manual (crbug.com/12345 )",
            (None, None, None),
            "not parse a bug with a trailing space",
        )
        expect(
            "Manual (crbug.com/12345)",
            (None, None, None),
            "not parse a value without https://",
        )
        expect(
            "Manual (https://crbug/12345)",
            (None, None, None),
            "not parse a value without .com",
        )
        expect(
            "Manual (http://crbug.com/12345)",
            (None, None, None),
            "not parse a value without https://",
        )


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