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

import base64
import json
import os
import shutil
import tempfile
import unittest

import merge_js_lib as merger
from parameterized import parameterized


class MergeJSLibTest(unittest.TestCase):
    def test_write_parsed_scripts(self):
        test_files = [{
            'url': '//a/b/c/1.js',
            'location': ['a', 'b', 'c', '1.js'],
            'exists': True
        }, {
            'url': '//d/e/f/5.js',
            'location': ['d', 'e', 'f', '5.js'],
            'exists': True
        }, {
            'url': '//a/b/d/7.js',
            'location': ['a', 'b', 'd', '7.js'],
            'exists': True
        }, {
            'url': 'chrome://test_webui/file.js',
            'exists': False
        }, {
            'url': 'file://testing/file.js',
            'exists': False
        }]

        test_script_file = """{
"text": "test\\ncontents\\n%d",
"url": "%s",
"sourceMapURL":"%s"
}"""

        scripts_dir = None
        expected_files = []

        try:
            scripts_dir = tempfile.mkdtemp()
            for i, test_script in enumerate(test_files):
                file_path = os.path.join(scripts_dir, '%d.js.json' % i)

                source_map = ""
                if test_script['exists']:
                    # Create an inline sourcemap with just the required keys.
                    source_map_data_url = base64.b64encode(
                        json.dumps({
                            "sources":
                            [os.path.join(*test_script['location'])],
                            "sourceRoot":
                            ""
                        }).encode('utf-8'))

                    source_map = 'data:application/json;base64,' + \
                        source_map_data_url.decode('utf-8')

                with open(file_path, 'w') as f:
                    f.write(test_script_file %
                            (i, test_script['url'], source_map))

                expected_files.append(file_path)
                if test_script['exists']:
                    expected_files.append(
                        os.path.join(scripts_dir, 'parsed_scripts',
                                     *test_script['location']))

            if len(expected_files) > 0:
                expected_files.append(
                    os.path.join(scripts_dir, 'parsed_scripts',
                                 'parsed_scripts.json'))

            merger.write_parsed_scripts(scripts_dir, source_dir='')
            actual_files = []

            for root, _, files in os.walk(scripts_dir):
                for file_name in files:
                    actual_files.append(os.path.join(root, file_name))

            self.assertCountEqual(expected_files, actual_files)
        finally:
            shutil.rmtree(scripts_dir)

    def test_write_parsed_scripts_negative_cases(self):
        test_files = [{
            'url': '//a/b/c/1.js',
            'contents': """{
"url": "%s"
}"""
        }, {
            'url': '//d/e/f/1.js',
            'contents': """{
"text": "test\\ncontents\\n%s"
}"""
        }]

        scripts_dir = None
        expected_files = []
        try:
            scripts_dir = tempfile.mkdtemp()
            for i, test_script in enumerate(test_files):
                file_path = os.path.join(scripts_dir, '%d.js.json' % i)
                expected_files.append(file_path)
                with open(file_path, 'w') as f:
                    f.write(test_script['contents'] % test_script['url'])

            merger.write_parsed_scripts(scripts_dir)

            actual_files = []
            for root, _, files in os.walk(scripts_dir):
                for file_name in files:
                    actual_files.append(os.path.join(root, file_name))

            self.assertCountEqual(expected_files, actual_files)
        finally:
            shutil.rmtree(scripts_dir)

    def test_trailing_curly_brace_stripped(self):
        test_script_file = """{
  "text":"test\\ncontents\\n0",
  "url":"//a/b/c/1.js",
  "sourceMapURL":"data:application/json;base64,eyJzb3VyY2VzIjogWyJhL2IvYy8xLmpzIl0sICJzb3VyY2VSb290IjogIiJ9"
}}"""

        scripts_dir = None

        try:
            scripts_dir = tempfile.mkdtemp()
            file_path = os.path.join(scripts_dir, '0.js.json')
            with open(file_path, 'w') as f:
                f.write(test_script_file)
            expected_files = [
                file_path,
                os.path.join(scripts_dir, 'parsed_scripts', 'a', 'b', 'c',
                             '1.js'),
                os.path.join(scripts_dir, 'parsed_scripts',
                             'parsed_scripts.json')
            ]

            merger.write_parsed_scripts(scripts_dir, source_dir='')
            actual_files = []

            for root, _, files in os.walk(scripts_dir):
                for file_name in files:
                    actual_files.append(os.path.join(root, file_name))

            self.assertCountEqual(expected_files, actual_files)
        finally:
            shutil.rmtree(scripts_dir)

    def test_non_data_urls_are_ignored(self):
        test_script_file = """{
"text": "test\\ncontents",
"url": "http://test_url",
"sourceMapURL":"%s"
}"""

        scripts_dir = None
        expected_files = []

        try:
            scripts_dir = tempfile.mkdtemp()
            file_path = os.path.join(scripts_dir, 'external_map.js.json')
            expected_files = [file_path]

            # Write a script with an external URL as the sourcemap, this should
            # exclude it from being written to disk.
            with open(file_path, 'w') as f:
                f.write(test_script_file % 'external.map')

            merger.write_parsed_scripts(scripts_dir, source_dir='')
            actual_files = []

            for root, _, files in os.walk(scripts_dir):
                for file_name in files:
                    actual_files.append(os.path.join(root, file_name))

            self.assertCountEqual(expected_files, actual_files)
        finally:
            shutil.rmtree(scripts_dir)

    def test_uninteresting_lines_are_excluded(self):
        """This contrived istanbul coverage file represents the coverage from
        the following example file:
        """
        example_test_file = """// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import './iframe.js';

/*
 * function comment should be excluded.
 */
export const add = (a, b) => a + b; // should not be excluded

/* should be excluded */

"""

        test_istanbul_file = """{
"%s":{
  "path":"%s",
  "all":false,
  "statementMap":{
    "1":{"start":{"line":1,"column":0},"end":{"line":1,"column":38}},
    "2":{"start":{"line":2,"column":0},"end":{"line":2,"column":73}},
    "3":{"start":{"line":3,"column":0},"end":{"line":3,"column":29}},
    "4":{"start":{"line":4,"column":0},"end":{"line":4,"column":0}},
    "5":{"start":{"line":5,"column":0},"end":{"line":5,"column":21}},
    "6":{"start":{"line":6,"column":0},"end":{"line":6,"column":0}},
    "7":{"start":{"line":7,"column":0},"end":{"line":7,"column":2}},
    "8":{"start":{"line":8,"column":0},"end":{"line":8,"column":39}},
    "9":{"start":{"line":9,"column":0},"end":{"line":9,"column":3}},
    "10":{"start":{"line":10,"column":0},"end":{"line":10,"column":61}},
    "11":{"start":{"line":11,"column":0},"end":{"line":11,"column":0}},
    "12":{"start":{"line":12,"column":0},"end":{"line":12,"column":24}},
    "13":{"start":{"line":13,"column":0},"end":{"line":13,"column":0}}
  },
  "s":{
    "1": 1,
    "2": 1,
    "3": 1,
    "4": 1,
    "5": 1,
    "6": 1,
    "7": 1,
    "8": 1,
    "9": 1,
    "10": 1,
    "11": 1,
    "12": 1,
    "13": 1
  }
}
        }"""

        expected_output_file = """{
            "%s": {
                "path": "%s",
                "all": false,
                "statementMap": {
                    "10": {
                        "start": {
                            "line": 10,
                            "column": 0
                        },
                        "end": {
                            "line": 10,
                            "column": 61
                        }
                    }
                },
                "s": {
                    "10": 1
                }
            }
        }"""

        try:
            test_dir = tempfile.mkdtemp()
            file_path = os.path.join(test_dir,
                                     'coverage.json').replace('\\', '/')
            example_test_file_path = os.path.join(test_dir,
                                                  'fileA.js').replace(
                                                      '\\', '/')
            expected_output = json.loads(
                expected_output_file %
                (example_test_file_path, example_test_file_path))

            # Set up the tests files so that exclusions can be performed.
            with open(file_path, 'w') as f:
                f.write(test_istanbul_file %
                        (example_test_file_path, example_test_file_path))
            with open(example_test_file_path, 'w') as f:
                f.write(example_test_file)

            # Perform the exclusion.
            merger.exclude_uninteresting_lines(file_path)

            # Assert the final `coverage.json` file matches the expected output.
            with open(file_path, 'rb') as f:
                coverage_json = json.load(f)
                self.assertEqual(coverage_json, expected_output)

        finally:
            shutil.rmtree(test_dir)

    def test_paths_are_remapped_and_removed(self):
        test_file_data = """{
          "/path/to/checkout/chrome/browser/fileA.js": {
            "path": "/path/to/checkout/chrome/browser/fileA.js"
          },
          "/path/to/checkout/out/dir/chrome/browser/fileB.js": {
            "path": "/path/to/checkout/out/dir/chrome/browser/fileB.js"
          },
          "/some/random/path/fileC.js": {
            "path": "/some/random/path/fileC.js"
          }
        }"""

        expected_after_remap = {
            "chrome/browser/fileA.js": {
                "path": "chrome/browser/fileA.js"
            }
        }

        try:
            test_dir = tempfile.mkdtemp()
            coverage_file_path = os.path.join(test_dir,
                                              'coverage.json').replace(
                                                  '\\', '/')

            with open(coverage_file_path, 'w', encoding='utf-8',
                      newline='') as f:
                f.write(test_file_data)

            merger.remap_paths_to_relative(coverage_file_path,
                                           "/path/to/checkout",
                                           "/path/to/checkout/out/dir")

            with open(coverage_file_path, 'rb') as f:
                coverage_json = json.load(f)
                self.assertEqual(coverage_json, expected_after_remap)

        finally:
            shutil.rmtree(test_dir)

    @parameterized.expand([
        ('// test', True),
        ('/* test', True),
        ('*/ test', True),
        (' * test', True),
        ('import test', True),
        (' x = 5 /* comment */', False),
        ('x = 5', False),
    ])
    def test_should_exclude(self, line, exclude):
        self.assertEqual(merger.should_exclude(line), exclude)


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