#!/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 random
import unittest

import add_header


class DecoratedFilenameTest(unittest.TestCase):
  def testCHeaderClassification(self):
    self.assertTrue(add_header.IsCSystemHeader('<stdlib.h>'))
    self.assertFalse(add_header.IsCSystemHeader('<type_traits>'))
    self.assertFalse(add_header.IsCSystemHeader('"moo.h"'))

  def testCXXHeaderClassification(self):
    self.assertFalse(add_header.IsCXXSystemHeader('<stdlib.h>'))
    self.assertTrue(add_header.IsCXXSystemHeader('<type_traits>'))
    self.assertFalse(add_header.IsCXXSystemHeader('"moo.h"'))

  def testUserHeaderClassification(self):
    self.assertFalse(add_header.IsUserHeader('<stdlib.h>'))
    self.assertFalse(add_header.IsUserHeader('<type_traits>'))
    self.assertTrue(add_header.IsUserHeader('"moo.h"'))

  def testClassifyHeader(self):
    self.assertEqual(
      add_header.ClassifyHeader('<stdlib.h>'), add_header._HEADER_TYPE_C_SYSTEM
    )
    self.assertEqual(
      add_header.ClassifyHeader('<type_traits>'),
      add_header._HEADER_TYPE_CXX_SYSTEM,
    )
    self.assertEqual(
      add_header.ClassifyHeader('"moo.h"'), add_header._HEADER_TYPE_USER
    )
    self.assertEqual(
      add_header.ClassifyHeader('invalid'), add_header._HEADER_TYPE_INVALID
    )


class FindIncludesTest(unittest.TestCase):
  def testEmpty(self):
    begin, end = add_header.FindIncludes([])
    self.assertEqual(begin, -1)
    self.assertEqual(end, -1)

  def testNoIncludes(self):
    begin, end = add_header.FindIncludes(['a'])
    self.assertEqual(begin, -1)
    self.assertEqual(end, -1)

  def testOneInclude(self):
    begin, end = add_header.FindIncludes(['#include <algorithm>'])
    self.assertEqual(begin, 0)
    self.assertEqual(end, 1)

  def testIncludeWithInlineComment(self):
    begin, end = add_header.FindIncludes(
      ['#include "moo.h"  // TODO: Add more sounds.']
    )
    self.assertEqual(begin, 0)
    self.assertEqual(end, 1)

  def testNewlinesBetweenIncludes(self):
    begin, end = add_header.FindIncludes(
      ['#include <utility>', '', '#include "moo.h"']
    )
    self.assertEqual(begin, 0)
    self.assertEqual(end, 3)

  def testCommentsBetweenIncludes(self):
    begin, end = add_header.FindIncludes(
      ['#include <utility>', '// TODO: Add goat support.', '#include "moo.h"']
    )
    self.assertEqual(begin, 0)
    self.assertEqual(end, 3)

  def testEmptyLinesNotIncluded(self):
    begin, end = add_header.FindIncludes(
      ['', '#include <utility>', '', '#include "moo.h"', '']
    )
    self.assertEqual(begin, 1)
    self.assertEqual(end, 4)

  def testCommentsNotIncluded(self):
    begin, end = add_header.FindIncludes(
      [
        '// Cow module.',
        '#include <utility>',
        '// For cow speech synthesis.',
        '#include "moo.h"',
        '// TODO: Add Linux audio support.',
      ]
    )
    self.assertEqual(begin, 1)
    self.assertEqual(end, 4)

  def testNonIncludesLinesBeforeIncludesIgnored(self):
    begin, end = add_header.FindIncludes(
      ['#ifndef COW_H_', '#define COW_H_', '#include "moo.h"']
    )
    self.assertEqual(begin, 2)
    self.assertEqual(end, 3)

  def testNonIncludesLinesAfterIncludesTerminates(self):
    begin, end = add_header.FindIncludes(
      ['#include "moo.h"', '#ifndef COW_MESSAGES_H_', '#define COW_MESSAGE_H_']
    )
    self.assertEqual(begin, 0)
    self.assertEqual(end, 1)


class IncludeTest(unittest.TestCase):
  def testToSource(self):
    self.assertEqual(
      add_header.Include('<moo.h>', 'include', [], None).ToSource(),
      ['#include <moo.h>'],
    )

  def testIncludeWithPreambleToSource(self):
    self.assertEqual(
      add_header.Include(
        '"moo.h"', 'include', ['// preamble'], None
      ).ToSource(),
      ['// preamble', '#include "moo.h"'],
    )

  def testIncludeWithInlineCommentToSource(self):
    self.assertEqual(
      add_header.Include(
        '"moo.h"', 'include', [], ' inline comment'
      ).ToSource(),
      ['#include "moo.h"  // inline comment'],
    )

  def testIncludeWithPreambleAndInlineCommentToSource(self):
    # Make sure whitespace is vaguely normalized too.
    self.assertEqual(
      add_header.Include(
        '"moo.h"',
        'include',
        [
          '// preamble with trailing space ',
        ],
        ' inline comment with trailing space ',
      ).ToSource(),
      [
        '// preamble with trailing space',
        '#include "moo.h"  // inline comment with trailing space',
      ],
    )

  def testImportToSource(self):
    self.assertEqual(
      add_header.Include('"moo.h"', 'import', [], None).ToSource(),
      ['#import "moo.h"'],
    )


class ParseIncludesTest(unittest.TestCase):
  def testInvalid(self):
    self.assertIsNone(add_header.ParseIncludes(['invalid']))

  def testInclude(self):
    includes = add_header.ParseIncludes(['#include "moo.h"'])
    self.assertEqual(len(includes), 1)
    self.assertEqual(includes[0].decorated_name, '"moo.h"')
    self.assertEqual(includes[0].directive, 'include')
    self.assertEqual(includes[0].preamble, [])
    self.assertIsNone(includes[0].inline_comment)
    self.assertEqual(includes[0].header_type, add_header._HEADER_TYPE_USER)
    self.assertFalse(includes[0].is_primary_header)

  def testIncludeSurroundedByWhitespace(self):
    includes = add_header.ParseIncludes([' #include "moo.h" '])
    self.assertEqual(len(includes), 1)
    self.assertEqual(includes[0].decorated_name, '"moo.h"')
    self.assertEqual(includes[0].directive, 'include')
    self.assertEqual(includes[0].preamble, [])
    self.assertIsNone(includes[0].inline_comment)
    self.assertEqual(includes[0].header_type, add_header._HEADER_TYPE_USER)
    self.assertFalse(includes[0].is_primary_header)

  def testImport(self):
    includes = add_header.ParseIncludes(['#import "moo.h"'])
    self.assertEqual(len(includes), 1)
    self.assertEqual(includes[0].decorated_name, '"moo.h"')
    self.assertEqual(includes[0].directive, 'import')
    self.assertEqual(includes[0].preamble, [])
    self.assertIsNone(includes[0].inline_comment)
    self.assertEqual(includes[0].header_type, add_header._HEADER_TYPE_USER)
    self.assertFalse(includes[0].is_primary_header)

  def testIncludeWithPreamble(self):
    includes = add_header.ParseIncludes(
      ['// preamble comment ', '#include "moo.h"']
    )
    self.assertEqual(len(includes), 1)
    self.assertEqual(includes[0].decorated_name, '"moo.h"')
    self.assertEqual(includes[0].directive, 'include')
    self.assertEqual(includes[0].preamble, ['// preamble comment '])
    self.assertIsNone(includes[0].inline_comment)
    self.assertEqual(includes[0].header_type, add_header._HEADER_TYPE_USER)
    self.assertFalse(includes[0].is_primary_header)

  def testIncludeWithInvalidPreamble(self):
    self.assertIsNone(
      add_header.ParseIncludes(['// orphan comment', '', '#include "moo.h"'])
    )

  def testIncludeWIthInlineComment(self):
    includes = add_header.ParseIncludes(['#include "moo.h"// For SFX '])
    self.assertEqual(len(includes), 1)
    self.assertEqual(includes[0].decorated_name, '"moo.h"')
    self.assertEqual(includes[0].directive, 'include')
    self.assertEqual(includes[0].preamble, [])
    self.assertEqual(includes[0].inline_comment, ' For SFX ')
    self.assertEqual(includes[0].header_type, add_header._HEADER_TYPE_USER)
    self.assertFalse(includes[0].is_primary_header)

  def testIncludeWithInlineCommentAndPreamble(self):
    includes = add_header.ParseIncludes(
      ['// preamble comment ', '#include "moo.h"  // For SFX ']
    )
    self.assertEqual(len(includes), 1)
    self.assertEqual(includes[0].decorated_name, '"moo.h"')
    self.assertEqual(includes[0].directive, 'include')
    self.assertEqual(includes[0].preamble, ['// preamble comment '])
    self.assertEqual(includes[0].inline_comment, ' For SFX ')
    self.assertEqual(includes[0].header_type, add_header._HEADER_TYPE_USER)
    self.assertFalse(includes[0].is_primary_header)

  def testMultipleIncludes(self):
    includes = add_header.ParseIncludes(
      [
        '#include <time.h>',
        '',
        '#include "moo.h"  // For SFX ',
        '// TODO: Implement death ray.',
        '#import "goat.h"',
      ]
    )
    self.assertEqual(len(includes), 3)
    self.assertEqual(includes[0].decorated_name, '<time.h>')
    self.assertEqual(includes[0].directive, 'include')
    self.assertEqual(includes[0].preamble, [])
    self.assertIsNone(includes[0].inline_comment)
    self.assertEqual(includes[0].header_type, add_header._HEADER_TYPE_C_SYSTEM)
    self.assertFalse(includes[0].is_primary_header)
    self.assertEqual(includes[1].decorated_name, '"moo.h"')
    self.assertEqual(includes[1].directive, 'include')
    self.assertEqual(includes[1].preamble, [])
    self.assertEqual(includes[1].inline_comment, ' For SFX ')
    self.assertEqual(includes[1].header_type, add_header._HEADER_TYPE_USER)
    self.assertFalse(includes[1].is_primary_header)
    self.assertEqual(includes[2].decorated_name, '"goat.h"')
    self.assertEqual(includes[2].directive, 'import')
    self.assertEqual(includes[2].preamble, ['// TODO: Implement death ray.'])
    self.assertIsNone(includes[2].inline_comment)
    self.assertEqual(includes[2].header_type, add_header._HEADER_TYPE_USER)
    self.assertFalse(includes[2].is_primary_header)


class MarkPrimaryIncludeTest(unittest.TestCase):
  def _extract_primary_name(self, includes):
    for include in includes:
      if include.is_primary_header:
        return include.decorated_name

  def testNoOpOnHeader(self):
    includes = [add_header.Include('"cow.h"', 'include', [], None)]
    add_header.MarkPrimaryInclude(includes, 'cow.h')
    self.assertIsNone(self._extract_primary_name(includes))

  def testSystemHeaderNotMatched(self):
    includes = [add_header.Include('<cow.h>', 'include', [], None)]
    add_header.MarkPrimaryInclude(includes, 'cow.cc')
    self.assertIsNone(self._extract_primary_name(includes))

  def testExactMatch(self):
    includes = [
      add_header.Include('"cow.h"', 'include', [], None),
      add_header.Include('"cow_posix.h"', 'include', [], None),
    ]
    add_header.MarkPrimaryInclude(includes, 'cow.cc')
    self.assertEqual(self._extract_primary_name(includes), '"cow.h"')

  def testFuzzyMatch(self):
    includes = [add_header.Include('"cow.h"', 'include', [], None)]
    add_header.MarkPrimaryInclude(includes, 'cow_linux_unittest.cc')
    self.assertEqual(self._extract_primary_name(includes), '"cow.h"')

  def testFuzzymatchInReverse(self):
    includes = [add_header.Include('"cow.h"', 'include', [], None)]
    add_header.MarkPrimaryInclude(includes, 'cow_uitest_aura.cc')
    self.assertEqual(self._extract_primary_name(includes), '"cow.h"')

  def testFuzzyMatchDoesntMatchDifferentSuffixes(self):
    includes = [add_header.Include('"cow_posix.h"', 'include', [], None)]
    add_header.MarkPrimaryInclude(includes, 'cow_windows.cc')
    self.assertIsNone(self._extract_primary_name(includes))

  def testMarksMostSpecific(self):
    includes = [
      add_header.Include('"cow.h"', 'include', [], None),
      add_header.Include('"cow_posix.h"', 'include', [], None),
    ]
    add_header.MarkPrimaryInclude(includes, 'cow_posix.cc')
    self.assertEqual(self._extract_primary_name(includes), '"cow_posix.h"')

  def testFullPathMatch(self):
    includes = [add_header.Include('"zfs/impl/cow.h"', 'include', [], None)]
    add_header.MarkPrimaryInclude(includes, 'zfs/impl/cow.cc')
    self.assertEqual(self._extract_primary_name(includes), '"zfs/impl/cow.h"')

  def testTopmostDirectoryDoesNotMatch(self):
    includes = [add_header.Include('"animal/impl/cow.h"', 'include', [], None)]
    add_header.MarkPrimaryInclude(includes, 'zfs/impl/cow.cc')
    self.assertIsNone(self._extract_primary_name(includes))

  def testSubstantiallySimilarPaths(self):
    includes = [
      add_header.Include('"farm/public/animal/cow.h"', 'include', [], None)
    ]
    add_header.MarkPrimaryInclude(includes, 'farm/animal/cow.cc')
    self.assertEqual(
      self._extract_primary_name(includes), '"farm/public/animal/cow.h"'
    )

  def testSubstantiallySimilarPathsAndExactMatch(self):
    includes = [
      add_header.Include(
        '"ui/gfx/ipc/geometry/gfx_param_traits.h"', 'include', [], None
      ),
      add_header.Include(
        '"ui/gfx/ipc/gfx_param_traits.h"', 'include', [], None
      ),
    ]
    add_header.MarkPrimaryInclude(includes, 'ui/gfx/ipc/gfx_param_traits.cc')
    self.assertEqual(
      self._extract_primary_name(includes), '"ui/gfx/ipc/gfx_param_traits.h"'
    )

  def testNoMatchingSubdirectories(self):
    includes = [add_header.Include('"base/zfs/cow.h"', 'include', [], None)]
    add_header.MarkPrimaryInclude(includes, 'base/animal/cow.cc')
    self.assertIsNone(self._extract_primary_name(includes))


class SerializeIncludesTest(unittest.TestCase):
  def testSystemHeaders(self):
    source = add_header.SerializeIncludes(
      [
        add_header.Include('<stdlib.h>', 'include', [], None),
        add_header.Include('<map>', 'include', [], None),
      ]
    )
    self.assertEqual(source, ['#include <stdlib.h>', '', '#include <map>'])

  def testUserHeaders(self):
    source = add_header.SerializeIncludes(
      [
        add_header.Include('"goat.h"', 'include', [], None),
        add_header.Include('"moo.h"', 'include', [], None),
      ]
    )
    self.assertEqual(source, ['#include "goat.h"', '#include "moo.h"'])

  def testSystemAndUserHeaders(self):
    source = add_header.SerializeIncludes(
      [
        add_header.Include('<stdlib.h>', 'include', [], None),
        add_header.Include('<map>', 'include', [], None),
        add_header.Include('"moo.h"', 'include', [], None),
      ]
    )
    self.assertEqual(
      source,
      ['#include <stdlib.h>', '', '#include <map>', '', '#include "moo.h"'],
    )

  def testPrimaryAndSystemHeaders(self):
    primary_header = add_header.Include('"cow.h"', 'include', [], None)
    primary_header.is_primary_header = True
    source = add_header.SerializeIncludes(
      [
        primary_header,
        add_header.Include('<stdlib.h>', 'include', [], None),
        add_header.Include('<map>', 'include', [], None),
      ]
    )
    self.assertEqual(
      source,
      ['#include "cow.h"', '', '#include <stdlib.h>', '', '#include <map>'],
    )

  def testPrimaryAndUserHeaders(self):
    primary_header = add_header.Include('"cow.h"', 'include', [], None)
    primary_header.is_primary_header = True
    source = add_header.SerializeIncludes(
      [
        primary_header,
        add_header.Include('"moo.h"', 'include', [], None),
      ]
    )
    self.assertEqual(source, ['#include "cow.h"', '', '#include "moo.h"'])

  def testPrimarySystemAndUserHeaders(self):
    primary_header = add_header.Include('"cow.h"', 'include', [], None)
    primary_header.is_primary_header = True
    source = add_header.SerializeIncludes(
      [
        primary_header,
        add_header.Include('<stdlib.h>', 'include', [], None),
        add_header.Include('<map>', 'include', [], None),
        add_header.Include('"moo.h"', 'include', [], None),
      ]
    )
    self.assertEqual(
      source,
      [
        '#include "cow.h"',
        '',
        '#include <stdlib.h>',
        '',
        '#include <map>',
        '',
        '#include "moo.h"',
      ],
    )

  def testSpecialHeaders(self):
    includes = []
    primary_header = add_header.Include('"cow.h"', 'include', [], None)
    primary_header.is_primary_header = True
    includes.append(primary_header)
    includes.append(add_header.Include('<objbase.h>', 'include', [], None))
    includes.append(add_header.Include('<atlbase.h>', 'include', [], None))
    includes.append(add_header.Include('<ole2.h>', 'include', [], None))
    includes.append(add_header.Include('<shobjidl.h>', 'include', [], None))
    includes.append(add_header.Include('<tchar.h>', 'include', [], None))
    includes.append(add_header.Include('<unknwn.h>', 'include', [], None))
    includes.append(add_header.Include('<winsock2.h>', 'include', [], None))
    includes.append(add_header.Include('<windows.h>', 'include', [], None))
    includes.append(add_header.Include('<ws2tcpip.h>', 'include', [], None))
    includes.append(add_header.Include('<stddef.h>', 'include', [], None))
    includes.append(add_header.Include('<stdio.h>', 'include', [], None))
    includes.append(add_header.Include('<string.h>', 'include', [], None))
    includes.append(add_header.Include('"moo.h"', 'include', [], None))
    random.shuffle(includes)
    source = add_header.SerializeIncludes(includes)
    self.assertEqual(
      source,
      [
        '#include "cow.h"',
        '',
        '#include <objbase.h>',
        '#include <atlbase.h>',
        '#include <ole2.h>',
        '#include <shobjidl.h>',
        '#include <tchar.h>',
        '#include <unknwn.h>',
        '#include <windows.h>',
        '#include <winsock2.h>',
        '#include <ws2tcpip.h>',
        '#include <stddef.h>',
        '#include <stdio.h>',
        '#include <string.h>',
        '',
        '#include "moo.h"',
      ],
    )


class AddHeaderToSourceTest(unittest.TestCase):
  def testAddInclude(self):
    source = add_header.AddHeaderToSource(
      'cow.cc',
      '\n'.join(
        [
          '// Copyright info here.',
          '',
          '#include <utility>',
          '// For cow speech synthesis.',
          '#include "moo.h"  // TODO: Add Linux audio support.',
          '#include <time.h>',
          '#include "cow.h"',
          'namespace bovine {',
          '',
          '// TODO: Implement.',
          '}  // namespace bovine',
        ]
      ),
      '<memory>',
    )
    self.assertEqual(
      source,
      '\n'.join(
        [
          '// Copyright info here.',
          '',
          '#include "cow.h"',
          '',
          '#include <time.h>',
          '',
          '#include <memory>',
          '#include <utility>',
          '',
          '// For cow speech synthesis.',
          '#include "moo.h"  // TODO: Add Linux audio support.',
          'namespace bovine {',
          '',
          '// TODO: Implement.',
          '}  // namespace bovine',
          '',
        ]
      ),
    )

  def testAlreadyIncluded(self):
    # To make sure the original source is returned unmodified, the input source
    # intentionally scrambles the #include order.
    source = '\n'.join(
      [
        '// Copyright info here.',
        '',
        '#include "moo.h"',
        '#include <utility>',
        '#include <memory>',
        '#include "cow.h"',
        'namespace bovine {',
        '',
        '// TODO: Implement.',
        '}  // namespace bovine',
      ]
    )
    self.assertEqual(
      add_header.AddHeaderToSource('cow.cc', source, '<memory>'), None
    )

  def testConditionalIncludesLeftALone(self):
    # TODO(dcheng): Conditional header handling could probably be more clever.
    # But for the moment, this is probably Good Enough.
    source = add_header.AddHeaderToSource(
      'cow.cc',
      '\n'.join(
        [
          '// Copyright info here.',
          '',
          '#include "cow.h"',
          '#include <utility>',
          '// For cow speech synthesis.',
          '#include "moo.h"  // TODO: Add Linux audio support.',
          '#if defined(USE_AURA)',
          '#include <memory>',
          '#endif  // defined(USE_AURA)',
        ]
      ),
      '<memory>',
    )
    self.assertEqual(
      source,
      '\n'.join(
        [
          '// Copyright info here.',
          '',
          '#include "cow.h"',
          '',
          '#include <memory>',
          '#include <utility>',
          '',
          '// For cow speech synthesis.',
          '#include "moo.h"  // TODO: Add Linux audio support.',
          '#if defined(USE_AURA)',
          '#include <memory>',
          '#endif  // defined(USE_AURA)',
          '',
        ]
      ),
    )

  def testRemoveInclude(self):
    source = add_header.AddHeaderToSource(
      'cow.cc',
      '\n'.join(
        [
          '// Copyright info here.',
          '',
          '#include <memory>',
          '#include <utility>',
          '// For cow speech synthesis.',
          '#include "moo.h"  // TODO: Add Linux audio support.',
          '#include <time.h>',
          '#include "cow.h"',
          'namespace bovine {',
          '',
          '// TODO: Implement.',
          '}  // namespace bovine',
        ]
      ),
      '<utility>',
      remove=True,
    )
    self.assertEqual(
      source,
      '\n'.join(
        [
          '// Copyright info here.',
          '',
          '#include "cow.h"',
          '',
          '#include <time.h>',
          '',
          '#include <memory>',
          '',
          '// For cow speech synthesis.',
          '#include "moo.h"  // TODO: Add Linux audio support.',
          'namespace bovine {',
          '',
          '// TODO: Implement.',
          '}  // namespace bovine',
          '',
        ]
      ),
    )


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