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

import json
import os
import shutil
import tempfile
import unittest

import merge_js_lib as merger
from pathlib import Path
import node

_HERE_DIR = Path(__file__).parent.resolve()
_SOURCE_MAP_PROCESSOR = (_HERE_DIR.parent.parent.parent /
                         'tools' / 'code_coverage' /
                         'js_source_maps' / 'create_js_source_maps' /
                         'create_js_source_maps.js').resolve()

@unittest.skipIf(os.name == 'nt', 'Not intended to work on Windows')
class ConvertToIstanbulTest(unittest.TestCase):
    _TEST_SOURCE_A = """function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

subtract(5, 2);
"""
    _INVALID_MAPPING_A = (
        "//# sourceMappingURL=data:application/json;base64,"
        "eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZvby50cyJdLCJuYW1lcyI6W10sIm1hcHBpb"
        "mdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Oz"
        "s7OztBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUN"
        "BIiwiZmlsZSI6Ii91c3IvbG9jYWwvZ29vZ2xlL2hvbWUvc3Jpbml2YXNoZWdkZS9jaHJv"
        "bWl1bS9zcmMvZm9vX3ByZS50cyIsInNvdXJjZVJvb3QiOiIvdXNyL2xvY2FsL2dvb2dsZ"
        "S9ob21lL3NyaW5pdmFzaGVnZGUvY2hyb21pdW0vc3JjIiwic291cmNlc0NvbnRlbnQiOl"
        "siZnVuY3Rpb24gYWRkKGEsIGIpIHtcbiAgcmV0dXJuIGEgKyBiO1xufVxuXG5mdW5jdGl"
        "vbiBzdWJ0cmFjdChhLCBiKSB7XG4gIHJldHVybiBhIC0gYjtcbn1cblxuc3VidHJhY3Qo"
        "NSwgMik7XG4iXX0="
        )

    _TEST_COVERAGE_A = """{
  "result": [
    {
      "scriptId":"72",
      "url":"//file.js",
      "functions":[
        {
          "functionName":"",
          "ranges":[
            {"startOffset":0,"endOffset":101,"count":1}
          ],
          "isBlockCoverage":true
        },
        {
          "functionName":"add",
          "ranges":[
            {"startOffset":0,"endOffset":38,"count":0}
          ],
          "isBlockCoverage":false
        },
        {
          "functionName":"subtract",
          "ranges":[
            {"startOffset":40,"endOffset":83,"count":1}
          ],
          "isBlockCoverage":true
        }
      ]
    }
  ]
}
"""

    _TEST_COVERAGE_INVALID = """{
  "scriptId":"72",
  "url":"//file.js",
  "functions":[
    {
      "functionName":"",
      "ranges":[
        {"startOffset":0,"endOffset":101,"count":1}
      ],
      "isBlockCoverage":true
    },
    {
      "functionName":"add",
      "ranges":[
        {"startOffset":0,"endOffset":38,"count":0}
      ],
      "isBlockCoverage":false
    },
    {
      "functionName":"subtract",
      "ranges":[
        {"startOffset":40,"endOffset":83,"count":1}
      ],
      "isBlockCoverage":true
    }
  ]
}
"""

    _TEST_SOURCE_B = """const {subtract} = require('./test1.js');

function add(a, b) {
  return a + b;
}

subtract(5, 2);

"""

    _TEST_SOURCE_C = """exports.subtract = function(a, b) {
  return a - b;
}
"""

    _TEST_COVERAGE_B = """{
  "result":[
    {
      "scriptId":"72",
      "url":"//test.js",
      "functions":[
        {
          "functionName":"",
          "ranges":[
            {"startOffset":0,"endOffset":99,"count":1}
          ],
          "isBlockCoverage":true
        },
        {
          "functionName":"add",
          "ranges":[
            {"startOffset":43,"endOffset":81,"count":0}
          ],
          "isBlockCoverage":false
        }
      ]
    },
    {
      "scriptId":"73",
      "url":"//test1.js",
      "functions":[
        {
          "functionName":"",
          "ranges":[
            {"startOffset":0,"endOffset":54,"count":1}
          ],
          "isBlockCoverage":true
        },
        {
          "functionName":"exports.subtract",
          "ranges":[
            {"startOffset":19,"endOffset":53,"count":1}
          ],
          "isBlockCoverage":true
        }
      ]
    }
  ]
}
"""

    _TEST_COVERAGE_NO_LEADING_SLASH = """{
  "result":[
    {
      "scriptId":"72",
      "url":"file:///usr/local/google/home/benreich/v8-to-istanbul/test.js",
      "functions":[
        {
          "functionName":"",
          "ranges":[
            {"startOffset":0,"endOffset":99,"count":1}
          ],
          "isBlockCoverage":true
        },
        {
          "functionName":"add",
          "ranges":[
            {"startOffset":43,"endOffset":81,"count":0}
          ],
          "isBlockCoverage":false
        }
      ]
    },
    {
      "scriptId":"73",
      "url":"//test1.js",
      "functions":[
        {
          "functionName":"",
          "ranges":[
            {"startOffset":0,"endOffset":54,"count":1}
          ],
          "isBlockCoverage":true
        },
        {
          "functionName":"exports.subtract",
          "ranges":[
            {"startOffset":19,"endOffset":53,"count":1}
          ],
          "isBlockCoverage":true
        }
      ]
    }
  ]
}
"""

    _TEST_COVERAGE_DUPLICATE_SINGLE = """{
  "result":[
    {
      "scriptId":"73",
      "url":"//test1.js",
      "functions":[
        {
          "functionName":"",
          "ranges":[
            {"startOffset":0,"endOffset":54,"count":1}
          ],
          "isBlockCoverage":true
        },
        {
          "functionName":"exports.subtract",
          "ranges":[
            {"startOffset":19,"endOffset":53,"count":1}
          ],
          "isBlockCoverage":true
        }
      ]
    }
  ]
}
"""

    _TEST_COVERAGE_DUPLICATE_DOUBLE = """{
  "result":[
    {
      "scriptId":"72",
      "url":"//test.js",
      "functions":[
        {
          "functionName":"",
          "ranges":[
            {"startOffset":0,"endOffset":99,"count":1}
          ],
          "isBlockCoverage":true
        },
        {
          "functionName":"add",
          "ranges":[
            {"startOffset":43,"endOffset":81,"count":0}
          ],
          "isBlockCoverage":false
        }
      ]
    },
    {
      "scriptId":"73",
      "url":"//test1.js",
      "functions":[
        {
          "functionName":"",
          "ranges":[
            {"startOffset":0,"endOffset":54,"count":1}
          ],
          "isBlockCoverage":true
        },
        {
          "functionName":"exports.subtract",
          "ranges":[
            {"startOffset":19,"endOffset":53,"count":1}
          ],
          "isBlockCoverage":true
        }
      ]
    }
  ]
}
"""

    def setUp(self):
        self.task_output_dir = tempfile.mkdtemp()
        self.coverage_dir = os.path.join(self.task_output_dir, 'coverages')
        self.source_dir = os.path.join(self.task_output_dir, 'source')
        self.out_dir = os.path.join(self.task_output_dir, 'out')
        self.sourceRoot = '/'

        os.makedirs(self.coverage_dir)
        os.makedirs(self.source_dir)
        os.makedirs(self.out_dir)

    def tearDown(self):
        shutil.rmtree(self.task_output_dir)

    def list_files(self, absolute_path):
        actual_files = []
        for root, _, files in os.walk(absolute_path):
            actual_files.extend(
                [os.path.join(root, file_name) for file_name in files])

        return actual_files

    def _write_files(self, root_dir, *file_path_contents):
        for data in file_path_contents:
            file_path, contents = data
            with open(os.path.join(root_dir, file_path), 'w') as f:
                f.write(contents)

    def _write_transformations(
        self, source_dir, out_dir,
        original_file_name, input_file_name, output_file_name):
        original_file = os.path.join(source_dir, original_file_name)
        input_file = os.path.join(source_dir, input_file_name)
        output_file = os.path.join(out_dir, output_file_name)
        node.RunNode([
            str(_SOURCE_MAP_PROCESSOR),
            "--originals={}".format(" ".join([original_file])),
            "--inputs={}".format(" ".join([input_file])),
            "--outputs={}".format(" ".join([output_file])),
            "--inline-sourcemaps",
            "--sourceRoot={}".format(self.sourceRoot),
        ])

    def write_sources(self, *file_path_contents):
        url_to_path_map = {}
        for path_url, contents in file_path_contents:
            file_path, url = path_url
            url_to_path_map[file_path] = url
            self._write_files(self.source_dir, (url, contents))
            self._write_files(self.out_dir, (url, contents))
            self._write_transformations(
              self.source_dir, self.out_dir, url, url, url)
        with open(os.path.join(self.out_dir, 'parsed_scripts.json'),
                  'w',
                  encoding='utf-8') as f:
            f.write(json.dumps(url_to_path_map))

    def write_coverages(self, *file_path_contents):
        self._write_files(self.coverage_dir, *file_path_contents)

    def test_happy_path(self):
        self.write_sources((('//file.js', 'file.js'), self._TEST_SOURCE_A))
        self.write_coverages(('test_coverage.cov.json', self._TEST_COVERAGE_A))

        merger.convert_raw_coverage_to_istanbul([self.coverage_dir],
                                                self.out_dir,
                                                self.task_output_dir)

        istanbul_files = self.list_files(
            os.path.join(self.task_output_dir, 'istanbul'))
        self.assertEqual(len(istanbul_files), 1)

    def test_invalid_mapping(self):
        self.write_sources((('//file.js', 'file.js'), self._TEST_SOURCE_A))
        self._write_files(
            self.out_dir, (
            'file.js', self._TEST_SOURCE_A + '\n' + self._INVALID_MAPPING_A))
        self.write_coverages(('test_coverage.cov.json', self._TEST_COVERAGE_A))


        merger.convert_raw_coverage_to_istanbul([self.coverage_dir],
                                                self.out_dir,
                                                self.task_output_dir)

        istanbul_files = self.list_files(
            os.path.join(self.task_output_dir, 'istanbul'))
        self.assertEqual(len(istanbul_files), 0)

    def test_no_coverages_in_file(self):
        coverage_file = """{
      "result": []
    }
    """

        self.write_sources((('//file.js', 'file.js'), self._TEST_SOURCE_A))
        self.write_coverages(('test_coverage.cov.json', coverage_file))

        merger.convert_raw_coverage_to_istanbul([self.coverage_dir],
                                                self.out_dir,
                                                self.task_output_dir)

        istanbul_files = self.list_files(
            os.path.join(self.task_output_dir, 'istanbul'))
        self.assertEqual(len(istanbul_files), 0)

    def test_invalid_coverage_file(self):
        self.write_sources((('//file.js', 'file.js'), self._TEST_SOURCE_A))
        self.write_coverages(
            ('test_coverage.cov.json', self._TEST_COVERAGE_INVALID))

        with self.assertRaises(RuntimeError):
            merger.convert_raw_coverage_to_istanbul([self.coverage_dir],
                                                    self.out_dir,
                                                    self.task_output_dir)

    def test_multiple_coverages_single_file(self):
        self.write_sources((('//test.js', 'test.js'), self._TEST_SOURCE_B),
                           (('//test1.js', 'test1.js'), self._TEST_SOURCE_C))
        self.write_coverages(('test_coverage.cov.json', self._TEST_COVERAGE_B))

        merger.convert_raw_coverage_to_istanbul([self.coverage_dir],
                                                self.out_dir,
                                                self.task_output_dir)

        istanbul_files = self.list_files(
            os.path.join(self.task_output_dir, 'istanbul'))
        self.assertEqual(len(istanbul_files), 2)

    def test_multiple_coverages_no_leading_double_slash(self):
        self.write_sources((('//test.js', 'test.js'), self._TEST_SOURCE_B),
                           (('//test1.js', 'test1.js'), self._TEST_SOURCE_C))
        self.write_coverages(
            ('test_coverage.cov.json', self._TEST_COVERAGE_NO_LEADING_SLASH))

        merger.convert_raw_coverage_to_istanbul([self.coverage_dir],
                                                self.out_dir,
                                                self.task_output_dir)

        istanbul_files = self.list_files(
            os.path.join(self.task_output_dir, 'istanbul'))
        self.assertEqual(len(istanbul_files), 1)


    def test_multiple_duplicate_coverages_flattened(self):
        self.write_sources((('//test.js', 'test.js'), self._TEST_SOURCE_B),
                           (('//test1.js', 'test1.js'), self._TEST_SOURCE_C))
        self.write_coverages(
            ('test_coverage_1.cov.json', self._TEST_COVERAGE_B))
        self.write_coverages(
            ('test_coverage_2.cov.json', self._TEST_COVERAGE_DUPLICATE_DOUBLE))

        merger.convert_raw_coverage_to_istanbul([self.coverage_dir],
                                                self.out_dir,
                                                self.task_output_dir)

        istanbul_files = self.list_files(
            os.path.join(self.task_output_dir, 'istanbul'))
        self.assertEqual(len(istanbul_files), 2)


    def test_original_source_missing(self):
        self.write_sources((('//file.js', 'file.js'), self._TEST_SOURCE_A))
        self.write_coverages(('test_coverage.cov.json', self._TEST_COVERAGE_A))
        os.remove(os.path.join(self.source_dir, "file.js"))

        merger.convert_raw_coverage_to_istanbul([self.coverage_dir],
                                                self.out_dir,
                                                self.task_output_dir)

        istanbul_files = self.list_files(
            os.path.join(self.task_output_dir, 'istanbul'))
        self.assertEqual(len(istanbul_files), 0)


    def test_multiple_coverages_in_multiple_shards(self):
        coverage_dir_1 = os.path.join(self.coverage_dir, 'coverage1')
        coverage_dir_2 = os.path.join(self.coverage_dir, 'coverage2')
        os.makedirs(coverage_dir_1)
        os.makedirs(coverage_dir_2)

        self.write_sources((('//test.js', 'test.js'), self._TEST_SOURCE_B),
                           (('//test1.js', 'test1.js'), self._TEST_SOURCE_C))
        self._write_files(coverage_dir_1,
                          ('test_coverage_1.cov.json', self._TEST_COVERAGE_B))
        self._write_files(
            coverage_dir_2,
            ('test_coverage_2.cov.json', self._TEST_COVERAGE_DUPLICATE_DOUBLE))

        merger.convert_raw_coverage_to_istanbul(
            [coverage_dir_1, coverage_dir_2], self.out_dir,
            self.task_output_dir)

        istanbul_files = self.list_files(
            os.path.join(self.task_output_dir, 'istanbul'))
        self.assertEqual(len(istanbul_files), 2)


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