#!/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

# vpython-provided modules.
from parameterized import parameterized  # pylint: disable=import-error

import merge_js_lib as merger


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