#!/usr/bin/env python3
# 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 ts_library
import ts_definitions
import path_mappings
import os
import shutil
import tempfile
import unittest

_HERE_DIR = os.path.dirname(__file__)
_CWD = os.getcwd()


class TsLibraryTest(unittest.TestCase):
  def setUp(self):
    self._out_folder = None
    self._additional_flags = []
    self.maxDiff = None

  def tearDown(self):
    self._additional_flags = []
    if self._out_folder:
      shutil.rmtree(self._out_folder)

  def _read_file(self, parent_dir, file_name):
    file_path = os.path.join(parent_dir, file_name)
    with open(file_path, 'r', newline='') as f:
      return f.read()

  def _build_project1(self, enable_source_maps=False):
    gen_dir = os.path.join(
      self._out_folder, 'tools', 'typescript', 'tests', 'project1'
    )

    # Generate definition .d.ts file for legacy JS file.
    ts_definitions.main(
      [
        '--root_dir',
        os.path.join(_HERE_DIR, 'tests', 'project1'),
        '--gen_dir',
        gen_dir,
        '--out_dir',
        gen_dir,
        '--js_files',
        'legacy_file.js',
      ]
      + self._additional_flags
    )

    # Build project1, which includes a mix of TS and definition files.
    args = [
      '--output_suffix',
      'build_ts',
      '--root_gen_dir',
      os.path.relpath(self._out_folder, gen_dir),
      '--root_src_dir',
      os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
      '--root_dir',
      os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project1'), _CWD),
      '--gen_dir',
      os.path.relpath(gen_dir, _CWD),
      '--out_dir',
      os.path.relpath(gen_dir, _CWD),
      '--in_files',
      'foo.ts',
      '--definitions',
      'legacy_file.d.ts',
      '--composite',
    ]

    if enable_source_maps:
      args += ['--enable_source_maps']

    ts_library.main(args + self._additional_flags)
    return gen_dir

  def _assert_project1_output(self, gen_dir):
    files = [
      'foo.d.ts',
      'foo.js',
      'legacy_file.d.ts',
      'tsconfig_definitions.json',
      'tsconfig_build_ts.json',
      'build_ts_manifest.json',
    ]
    for f in files:
      self.assertTrue(os.path.exists(os.path.join(gen_dir, f)), f)

    # Check that the generated .tsbuildinfo file is deleted.
    tsbuildinfo = 'tsconfig_build_ts.tsbuildinfo'
    self.assertFalse(
      os.path.exists(os.path.join(gen_dir, tsbuildinfo)), tsbuildinfo
    )

  # Builds project2 which depends on files from project1 and project3, project6,
  # both via relative URLs, as well as via absolute chrome:// and
  # chrome://resources/ URLs.
  def _build_project2(
    self, project1_gen_dir, project3_gen_dir, project6_gen_dir
  ):
    root_dir = os.path.join(_HERE_DIR, 'tests', 'project2')
    gen_dir = os.path.join(
      self._out_folder, 'tools', 'typescript', 'tests', 'project2'
    )
    project1_gen_dir = os.path.relpath(project1_gen_dir, gen_dir)
    project3_gen_dir = os.path.relpath(project3_gen_dir, gen_dir)
    project6_gen_dir = os.path.relpath(project6_gen_dir, gen_dir)
    # Using path mappings to generate the path map file. path_mappings is also
    # unit tested separately in path_mappings_test.py.
    path_mappings.main(
      [
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--raw_deps',
        '//ui/webui/resources/js:build_ts',
        '--output_suffix',
        'project2',
      ]
    )

    ts_library.main(
      [
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(root_dir, _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'bar.ts',
        '--deps',
        os.path.join(project1_gen_dir, 'tsconfig_build_ts.json'),
        os.path.join(project3_gen_dir, 'tsconfig_build_ts.json'),
        os.path.join(project6_gen_dir, 'tsconfig_build_ts.json'),
        '--path_mappings',
        'chrome://some-other-source/*|' + os.path.join(project1_gen_dir, '*'),
        '--path_mappings_file',
        'path_mappings_project2.json',
        '--tsconfig_base',
        os.path.relpath(os.path.join(root_dir, 'tsconfig_base.json'), gen_dir),
      ]
      + self._additional_flags
    )
    return gen_dir

  def _assert_project2_output(self, gen_dir):
    files = [
      'bar.js',
      'tsconfig_build_ts.json',
      'build_ts_manifest.json',
      'path_mappings_project2.json',
    ]
    for f in files:
      self.assertTrue(os.path.exists(os.path.join(gen_dir, f)), f)

    non_existing_files = [
      'bar.d.ts',
      'tsconfig_build_ts.tsbuildinfo',
    ]
    for f in non_existing_files:
      self.assertFalse(os.path.exists(os.path.join(gen_dir, f)), f)

  # Builds project3, which includes only definition files.
  def _build_project3(self):
    gen_dir = os.path.join(
      self._out_folder, 'tools', 'typescript', 'tests', 'project3'
    )

    ts_library.main(
      [
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project3'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--definitions',
        os.path.relpath(
          os.path.join(_HERE_DIR, 'tests', 'project3', 'baz.d.ts'), gen_dir
        ),
        '--composite',
      ]
      + self._additional_flags
    )
    return gen_dir

  def _assert_project3_output(self, gen_dir):
    self.assertTrue(
      os.path.exists(os.path.join(gen_dir, 'tsconfig_build_ts.json'))
    )
    self.assertFalse(
      os.path.exists(os.path.join(gen_dir, 'tsconfig_build_ts.tsbuildinfo'))
    )
    self.assertFalse(
      os.path.exists(os.path.join(gen_dir, 'build_ts_manifest.json'))
    )

    # Check that 'skipLibCheck=true' was *not* automatically added.
    tsconfig_contents = self._read_file(gen_dir, 'tsconfig_build_ts.json')
    tsconfig = json.loads(tsconfig_contents)
    self.assertFalse('skipLibCheck' in tsconfig['compilerOptions'])

  def _build_project4(self):
    gen_dir = os.path.join(
      self._out_folder, 'tools', 'typescript', 'tests', 'project4'
    )

    # Build project4, which includes multiple TS files, only one of which should
    # be included in the manifest.
    ts_library.main(
      [
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project4'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'include.ts',
        'exclude.ts',
        '--manifest_excludes',
        'exclude.ts',
      ]
      + self._additional_flags
    )
    return gen_dir

  def _assert_project4_output(self, gen_dir):
    files = [
      'include.js',
      'exclude.js',
      'tsconfig_build_ts.json',
      'build_ts_manifest.json',
    ]
    for f in files:
      self.assertTrue(os.path.exists(os.path.join(gen_dir, f)), f)

    # Check that 'skipLibCheck=true' was automatically added.
    tsconfig_contents = self._read_file(gen_dir, 'tsconfig_build_ts.json')
    tsconfig = json.loads(tsconfig_contents)
    self.assertTrue(tsconfig['compilerOptions']['skipLibCheck'])

    # Check that the generated manifest file doesn't include exclude.js.
    manifest = os.path.join(gen_dir, 'build_ts_manifest.json')
    self._assert_manifest_files(manifest, ['include.js'])

  def _assert_manifest_files(self, manifest_path, expected_files):
    with open(manifest_path, 'r') as f:
      data = json.load(f)
      self.assertEqual(data['files'], expected_files)

  def _build_project5(self):
    gen_dir = os.path.join(
      self._out_folder, 'tools', 'typescript', 'tests', 'project5'
    )
    out_dir_test = os.path.join(self._out_folder, 'project5_test')

    # Build project5, which includes 2 TS projects one for prod and one for
    # test, it should generate different manifest, tsconfig and tsbuildinfo.
    # prod:
    ts_library.main(
      [
        '--output_suffix',
        'build_ts',
        '--composite',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project5'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'bar.ts',
      ]
      + self._additional_flags
    )

    # test:
    ts_library.main(
      [
        '--output_suffix',
        'test_build_ts',
        '--deps',
        os.path.join(gen_dir, 'tsconfig_build_ts.json'),
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project5'), _CWD),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(gen_dir, _CWD),
        '--in_files',
        'bar_test.ts',
      ]
      + self._additional_flags
    )

    return gen_dir

  def _assert_project5_output(self, gen_dir):
    # prod:
    self.assertTrue(
      os.path.exists(os.path.join(gen_dir, 'tsconfig_build_ts.json'))
    )
    manifest = os.path.join(gen_dir, 'build_ts_manifest.json')
    self.assertTrue(os.path.exists(manifest))
    self._assert_manifest_files(manifest, ['bar.js'])

    # test:
    self.assertTrue(
      os.path.exists(os.path.join(gen_dir, 'tsconfig_test_build_ts.json'))
    )
    manifest_test = os.path.join(gen_dir, 'test_build_ts_manifest.json')
    self.assertTrue(os.path.exists(manifest_test))
    self._assert_manifest_files(manifest_test, ['bar_test.js'])

  def _build_project6(self):
    gen_dir = os.path.join(
      self._out_folder,
      'tools',
      'typescript',
      'tests',
      'ui',
      'webui',
      'resources',
      'js',
    )
    out_dir = os.path.join(
      self._out_folder, 'ui', 'webui', 'resources', 'tsc', 'js'
    )

    # Build project6, which simulates the build setup and location of shared
    # ui/webui/resources/ projects, and is used to test the codepath that infers
    # |path_mappings| from |raw_deps|.
    ts_library.main(
      [
        '--output_suffix',
        'build_ts',
        '--root_gen_dir',
        os.path.relpath(self._out_folder, gen_dir),
        '--root_src_dir',
        os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
        '--root_dir',
        os.path.relpath(
          os.path.join(_HERE_DIR, 'tests', 'ui', 'webui', 'resources', 'js'),
          _CWD,
        ),
        '--gen_dir',
        os.path.relpath(gen_dir, _CWD),
        '--out_dir',
        os.path.relpath(out_dir, _CWD),
        '--in_files',
        'assert.ts',
        '--composite',
      ]
      + self._additional_flags
    )

    return (gen_dir, out_dir)

  def _assert_project6_output(self, gen_dir, out_dir):
    gen_dir_files = [
      'tsconfig_build_ts.json',
      'build_ts_manifest.json',
    ]
    for f in gen_dir_files:
      self.assertTrue(os.path.exists(os.path.join(gen_dir, f)), f)

    # Check that the generated .tsbuildinfo file is deleted.
    tsbuildinfo = 'tsconfig_build_ts.tsbuildinfo'
    self.assertFalse(
      os.path.exists(os.path.join(gen_dir, tsbuildinfo)), tsbuildinfo
    )

    out_dir_files = [
      'assert.d.ts',
      'assert.js',
    ]
    for f in out_dir_files:
      self.assertTrue(os.path.exists(os.path.join(out_dir, f)), f)

  # Test success case where both project1 and project2 are compiled successfully
  # and no errors are thrown.
  def _testSuccess(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)
    project1_gen_dir = self._build_project1()
    self._assert_project1_output(project1_gen_dir)

    project3_gen_dir = self._build_project3()
    self._assert_project3_output(project3_gen_dir)

    (project6_gen_dir, project6_out_dir) = self._build_project6()
    self._assert_project6_output(project6_gen_dir, project6_out_dir)

    project2_gen_dir = self._build_project2(
      project1_gen_dir, project3_gen_dir, project6_gen_dir
    )
    self._assert_project2_output(project2_gen_dir)

    project4_gen_dir = self._build_project4()
    self._assert_project4_output(project4_gen_dir)

    project5_gen_dir = self._build_project5()
    self._assert_project5_output(project5_gen_dir)

  def testSuccess_v6(self):
    self._testSuccess()

  def testSuccess_v7(self):
    self._additional_flags = ['--use_typescript_go']
    self._testSuccess()

  # Test error case where a type violation exists, ensure that an error is
  # thrown.
  def _testError(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)
    gen_dir = os.path.join(
      self._out_folder, 'tools', 'typescript', 'tests', 'project1'
    )
    try:
      ts_library.main(
        [
          '--output_suffix',
          'build_ts',
          '--root_gen_dir',
          os.path.relpath(self._out_folder, gen_dir),
          '--root_src_dir',
          os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
          '--root_dir',
          os.path.relpath(os.path.join(_HERE_DIR, 'tests', 'project1'), _CWD),
          '--gen_dir',
          os.path.relpath(gen_dir, _CWD),
          '--out_dir',
          os.path.relpath(gen_dir, _CWD),
          '--in_files',
          'errors.ts',
          '--composite',
        ]
        + self._additional_flags
      )
    except RuntimeError as err:
      self.assertTrue(
        'Type \'number\' is not assignable to type \'string\'' in str(err)
      )
      self.assertFalse(
        os.path.exists(os.path.join(gen_dir, 'tsconfig_build_ts.tsbuildinfo'))
      )
    else:
      self.fail('Failed to detect type error')

  def testError_v6(self):
    self._testError()

  def testError_v7(self):
    self._additional_flags = ['--use_typescript_go']
    self._testError()

  # Test error case where the project's tsconfig file is failing validation.
  def testTsConfigValidationError(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)
    root_dir = os.path.join(_HERE_DIR, 'tests', 'project5')
    gen_dir = os.path.join(
      self._out_folder, 'tools', 'typescript', 'tests', 'project5'
    )
    try:
      ts_library.main(
        [
          '--output_suffix',
          'build_ts',
          '--root_gen_dir',
          os.path.relpath(self._out_folder, gen_dir),
          '--root_src_dir',
          os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
          '--root_dir',
          os.path.relpath(root_dir, _CWD),
          '--gen_dir',
          os.path.relpath(gen_dir, _CWD),
          '--out_dir',
          os.path.relpath(gen_dir, _CWD),
          '--in_files',
          'bar.ts',
          '--tsconfig_base',
          os.path.relpath(
            os.path.join(root_dir, 'tsconfig_base.json'), gen_dir
          ),
        ]
      )
    except AssertionError as err:
      self.assertTrue(
        str(err)
        .replace('\\', '/')
        .startswith(
          'Invalid |composite| flag detected in '
          'tools/typescript/tests/project5/tsconfig_base.json.'
        )
      )
    else:
      self.fail('Failed to detect error')

  # Test error case where the project's tsconfig file inherits from another
  # tsconfig file that should fail validation.
  def testTsConfigValidationErrorInParent(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)
    root_dir = os.path.join(_HERE_DIR, 'tests', 'project5')
    gen_dir = os.path.join(
      self._out_folder, 'tools', 'typescript', 'tests', 'project5'
    )
    try:
      ts_library.main(
        [
          '--output_suffix',
          'build_ts',
          '--root_gen_dir',
          os.path.relpath(self._out_folder, gen_dir),
          '--root_src_dir',
          os.path.relpath(os.path.join(_HERE_DIR, 'tests'), gen_dir),
          '--root_dir',
          os.path.relpath(root_dir, _CWD),
          '--gen_dir',
          os.path.relpath(gen_dir, _CWD),
          '--out_dir',
          os.path.relpath(gen_dir, _CWD),
          '--in_files',
          'bar.ts',
          '--tsconfig_base',
          os.path.relpath(
            os.path.join(root_dir, 'tsconfig_base2.json'), gen_dir
          ),
        ]
      )
    except AssertionError as err:
      self.assertTrue(
        str(err)
        .replace('\\', '/')
        .startswith(
          'Invalid |composite| flag detected in '
          'tools/typescript/tests/project5/tsconfig_base.json.'
        )
      )
    else:
      self.fail('Failed to detect error')

  # Test case where |enable_source_maps| is specified.
  def testEnableSourceMaps(self):
    self._out_folder = tempfile.mkdtemp(dir=_CWD)

    expectations_dir = os.path.join(_HERE_DIR, 'tests', 'expected', 'project1')

    # Build project1 which source maps enabled.
    gen_dir = self._build_project1(enable_source_maps=True)

    # Assert output contains a source map.
    SOURCE_MAP_TOKEN = '//# sourceMappingURL=data:application/json;base64,'

    def _assert_source_map_exists(file_path):
      with open(file_path, 'r', newline='') as f:
        lines = f.readlines()
        self.assertTrue(lines[-1].startswith(SOURCE_MAP_TOKEN))

    _assert_source_map_exists(os.path.join(gen_dir, 'foo.js'))


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