#!/usr/bin/env python3
# Copyright 2023 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

_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)

import gclient_utils  # noqa: E402
import metadata.parse  # noqa: E402
import metadata.fields.known  # noqa: E402


class ParseTest(unittest.TestCase):
    def test_parse_single(self):
        """Check parsing works for a single dependency's metadata."""
        filepath = os.path.join(
            _THIS_DIR, "data", "README.chromium.test.single-valid"
        )
        content = gclient_utils.FileRead(filepath)
        all_metadata = metadata.parse.parse_content(content)

        self.assertEqual(len(all_metadata), 1)
        self.assertListEqual(
            all_metadata[0].get_entries(),
            [
                ("Name", "Test-A README for Chromium metadata"),
                ("Short Name", "metadata-test-valid"),
                (
                    "URL",
                    "https://www.example.com/metadata,\n"
                    "     https://www.example.com/parser",
                ),
                (
                    "Unknown Field",
                    "Should be extracted into a field, because the preceding URL\n"
                    "               field is structured, thus terminated by another field-like\n"
                    "               line, even if the field name isn't well known to us.",
                ),
                ("Version", "1.0.12"),
                ("Date", "2020-12-03"),
                ("License", "Apache, 2.0 and MIT"),
                ("License File", "LICENSE"),
                ("Security Critical", "yes"),
                ("Shipped", "yes"),
                ("CPEPrefix", "unknown"),
                (
                    "Description",
                    "A test metadata file, with a\n multi-line description.",
                ),
                ("Local Modifications", "None."),
            ],
        )

        # Check line numbers are recorded correctly.
        self.assertEqual(
            (1, 23), all_metadata[0].get_first_and_last_line_number()
        )

    def test_parse_multiple(self):
        """Check parsing works for multiple dependencies' metadata."""
        filepath = os.path.join(
            _THIS_DIR, "data", "README.chromium.test.multi-invalid"
        )
        content = gclient_utils.FileRead(filepath)
        all_metadata = metadata.parse.parse_content(content)

        # Dependency metadata with no entries at all are ignored.
        self.assertEqual(len(all_metadata), 3)
        # Check entries are added according to fields being one-liners.
        self.assertListEqual(
            all_metadata[0].get_entries(),
            [
                (
                    "Name",
                    "Test-A README for Chromium metadata (0 errors, 0 warnings)",
                ),
                ("Short Name", "metadata-test-valid"),
                (
                    "URL",
                    "https://www.example.com/metadata,\n"
                    "     https://www.example.com/parser",
                ),
                ("Version", "1.0.12"),
                ("Date", "2020-12-03"),
                ("License", "Apache-2.0, MIT"),
                ("License File", "LICENSE"),
                ("Security Critical", "yes"),
                ("Shipped", "yes"),
                ("Update Mechanism", "Autoroll"),
                ("CPEPrefix", "unknown"),
                (
                    "Description",
                    "A test metadata file, with a\n multi-line description.",
                ),
                ("Local Modifications", "None,\nEXCEPT:\n* nothing."),
            ],
        )
        self.assertEqual(
            (1, 21), all_metadata[0].get_first_and_last_line_number()
        )

        # Check the parser handles different casing for field names, and
        # strips leading and trailing whitespace from values.
        self.assertListEqual(
            all_metadata[1].get_entries(),
            [
                (
                    "Name",
                    "Test-B README for Chromium metadata (3 errors, 1 warning)",
                ),
                ("SHORT NAME", "metadata-test-invalid"),
                ("URL", "file://home/drive/chromium/src/metadata"),
                ("Version", "0"),
                ("Date", "2020-12-03"),
                ("Update Mechanism", "Autoroll"),
                ("License", "MIT"),
                ("Security critical", "yes"),
                ("Shipped", "Yes"),
                ("Description", ""),
                ("Local Modifications", "None."),
            ],
        )
        self.assertEqual(
            (25, 48), all_metadata[1].get_first_and_last_line_number()
        )

        # Check repeated fields persist in the metadata's entries.
        self.assertListEqual(
            all_metadata[2].get_entries(),
            [
                (
                    "Name",
                    "Test-C README for Chromium metadata (4 errors, 1 warning)",
                ),
                ("URL", "https://www.example.com/first"),
                ("URL", "https://www.example.com/second"),
                ("Update Mechanism", "Autoroll"),
                ("Version", "N/A"),
                ("Date", "2020-12-03"),
                ("License", "Custom license"),
                ("Security Critical", "yes"),
                (
                    "Description",
                    """Test metadata with multiple entries for one field, and
missing a mandatory field.
These are the expected errors (here for reference only):

1. Required field 'License Android Compatible' is missing.

2. Required field 'License File' is missing.

3. Required field 'Shipped' is missing.

4. Repeated fields: URL (2)

warnings:
1. License has a license not in the allowlist.
(see https://source.chromium.org/chromium/chromiu
m/tools/depot_tools/+/main:metadata/fields/custom/license_al
lowlist.py). Licenses not allowlisted: 'Custom license'.""",
                ),
            ],
        )
        self.assertEqual(
            (53, 79), all_metadata[2].get_first_and_last_line_number()
        )

    def test_parse_multiple_local_modifications(self):
        """Check parsing works for multiple dependencies, each with different local modifications."""
        filepath = os.path.join(
            _THIS_DIR, "data", "README.chromium.test.multi-local-modifications"
        )
        content = gclient_utils.FileRead(filepath)
        all_metadata = metadata.parse.parse_content(content)

        self.assertEqual(len(all_metadata), 4)

        self.assertListEqual(
            all_metadata[0].get_entries(),
            [
                ("Name", "Test package 1"),
                (
                    "Local Modifications",
                    "1. Modified X file\n2. Deleted Y file",
                ),
            ],
        )
        self.assertEqual(
            (1, 5), all_metadata[0].get_first_and_last_line_number()
        )

        self.assertListEqual(
            all_metadata[1].get_entries(),
            [
                ("Name", "Test package 2"),
                ("Local Modifications", "None"),
            ],
        )
        self.assertEqual(
            (9, 10), all_metadata[1].get_first_and_last_line_number()
        )

        self.assertListEqual(
            all_metadata[2].get_entries(),
            [
                ("Name", "Test package 3"),
                ("Local Modifications", "None."),
            ],
        )
        self.assertEqual(
            (14, 24), all_metadata[2].get_first_and_last_line_number()
        )

        self.assertListEqual(
            all_metadata[3].get_entries(),
            [
                ("Name", "Test package 4"),
                ("Local Modifications", "None,\nExcept modified file X."),
            ],
        )
        self.assertEqual(
            (28, 30), all_metadata[3].get_first_and_last_line_number()
        )

    def test_parse_per_field_line_numbers(self):
        """Check parsing marks the line numbers of each individual fields."""
        filepath = os.path.join(
            _THIS_DIR, "data", "README.chromium.test.single-valid"
        )
        content = gclient_utils.FileRead(filepath)
        all_metadata = metadata.parse.parse_content(content)

        self.assertEqual(len(all_metadata), 1)

        dm = all_metadata[0]
        field_spec = metadata.fields.known
        expected_line_numbers = {  # noqa: F841
            field_spec.NAME: [1],
            field_spec.SHORT_NAME: [2],
            field_spec.URL: [3, 4],
            field_spec.VERSION: [8],
            field_spec.DATE: [9],
            field_spec.LICENSE: [10],
            field_spec.LICENSE_FILE: [11],
            field_spec.SECURITY_CRITICAL: [12],
            field_spec.SHIPPED: [13],
            field_spec.CPE_PREFIX: [14],
            field_spec.DESCRIPTION: [16, 17, 18],
            field_spec.LOCAL_MODIFICATIONS: [20, 21],
        }
        self.assertEqual(
            dm.get_field_line_numbers(metadata.fields.known.NAME), [1]
        )

    def test_parse_mitigated(self):
        """Check parsing works for mitigated CVE entries."""
        filepath = os.path.join(
            _THIS_DIR, "data", "README.chromium.test.mitigated"
        )
        content = gclient_utils.FileRead(filepath)
        all_metadata = metadata.parse.parse_content(content)

        self.assertEqual(len(all_metadata), 1)

        # Check that the CVEs are properly parsed.
        self.assertDictEqual(
            all_metadata[0].mitigations,
            {
                "CVE-2011-4061": "This copy of DependencyA only includes rainbows\nthat spill beautifully over multiple lines and are handled\n ~~ Perfectly ~~\nEven: this line with colons that mentions CVE-2000-2000: an unrelated cve.",
                "CVE-2024-7255": "This copy of DependencyA only includes unicorns",
                "CVE-2024-7256": "This also doesn't apply because of good reasons",
            },
        )


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