#!/usr/bin/env python3
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""api_static_checks_unittest.py - Unittests for api_static_checks.py"""

import contextlib
import hashlib
import io
import os
import shutil
import subprocess
import sys
import tempfile
import unittest

REPOSITORY_ROOT = os.path.abspath(
    os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
sys.path.insert(0, REPOSITORY_ROOT)

from components.cronet.tools import api_static_checks  # pylint: disable=wrong-import-position
from components.cronet.tools import update_api  # pylint: disable=wrong-import-position
from components.cronet.tools import utils  # pylint: disable=wrong-import-position

# pylint: disable=useless-object-inheritance

ERROR_PREFIX_CHECK_API_CALLS = (
    """ERROR: Found the following calls from implementation classes through
       API classes.  These could fail if older API is used that
       does not contain newer methods.  Please call through a
       wrapper class from VersionSafeCallbacks.
""")

ERROR_PREFIX_UPDATE_API = ("""ERROR: This API was modified or removed:
             """)

ERROR_SUFFIX_UPDATE_API = ("""

       Cronet API methods and classes cannot be modified.
""")

CHECK_API_VERSION_PREFIX = (
    """DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT

""")

API_FILENAME = './android/api.txt'
API_VERSION_FILENAME = './android/api_version.txt'


@contextlib.contextmanager
def capture_output():
    # A contextmanger that collects the stdout and stderr of wrapped code

    oldout, olderr = sys.stdout, sys.stderr
    try:
        out = [io.StringIO(), io.StringIO()]
        sys.stdout, sys.stderr = out
        yield out
    finally:
        sys.stdout, sys.stderr = oldout, olderr
        out[0] = out[0].getvalue()
        out[1] = out[1].getvalue()


class ApiStaticCheckUnitTest(unittest.TestCase):

    def setUp(self):
        self.exe_path = os.path.join(REPOSITORY_ROOT, 'out')
        self.temp_dir = tempfile.mkdtemp(dir=self.exe_path)
        os.chdir(self.temp_dir)
        os.mkdir('android')
        utils.write_file(API_VERSION_FILENAME, '0')
        utils.write_file(API_FILENAME,
                         '}\nStamp: 7d9d25f71cb8a5aba86202540a20d405\n')
        shutil.copytree(os.path.dirname(__file__), 'tools')

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

    def make_jar(self, java, class_name):
        # Compile |java| wrapped in a class named |class_name| to a jar file and
        # return jar filename.

        java_filename = class_name + '.java'
        class_filenames = class_name + '*.class'
        jar_filename = class_name + '.jar'

        utils.write_file(java_filename,
                         'public class %s {%s}' % (class_name, java))
        utils.run([utils.JAVAC_PATH, java_filename], verbose=False)
        utils.run(f'{utils.JAR_PATH} cf {jar_filename} {class_filenames}',
                  verbose=False,
                  shell=True)
        return jar_filename

    def run_check_api_calls(self, api_java, impl_java):
        test = self

        class MockOpts(object):

            def __init__(self):
                self.api_jar = test.make_jar(api_java, 'Api')
                self.impl_jar = [test.make_jar(impl_java, 'Impl')]
                self.verbose = False

        opts = MockOpts()
        with capture_output() as return_output:
            return_code = api_static_checks.check_api_calls(opts)
        return [return_code, return_output[0]]

    def test_check_api_calls_success(self):
        # Test simple classes with functions
        self.assertEqual(self.run_check_api_calls('void a(){}', 'void b(){}'),
                         [True, ''])
        # Test simple classes with functions calling themselves
        self.assertEqual(
            self.run_check_api_calls('void a(){} void b(){a();}',
                                     'void c(){} void d(){c();}'), [True, ''])

    def test_check_api_calls_failure(self):
        # Test static call
        self.assertEqual(
            self.run_check_api_calls('public static void a(){}',
                                     'void b(){Api.a();}'),
            [False, ERROR_PREFIX_CHECK_API_CALLS + 'Impl/b -> Api/a:()V\n'])
        # Test virtual call
        self.assertEqual(
            self.run_check_api_calls('public void a(){}',
                                     'void b(){new Api().a();}'),
            [False, ERROR_PREFIX_CHECK_API_CALLS + 'Impl/b -> Api/a:()V\n'])

    def run_check_api_version(self, java):
        jar_path = self.make_jar(java, 'Api')
        try:
            output = utils.run_and_get_stdout(
                ['python3', './tools/update_api.py', '--api_jar', jar_path],
                verbose=False)
            return_code = 0
        except subprocess.CalledProcessError as e:
            output = e.stdout.decode('utf-8') + e.stderr.decode('utf-8')
            return_code = e.returncode

        api = utils.read_file(API_FILENAME)
        api_version = utils.read_file(API_VERSION_FILENAME)

        # Verify stamp
        api_stamp = api.split('\n')[-2]
        stamp_length = len('Stamp: 78418460c193047980ae9eabb79293f2\n')
        api = api[:-stamp_length]
        api_hash = hashlib.md5()
        api_hash.update(api.encode('utf-8'))
        self.assertEqual(api_stamp, 'Stamp: %s' % api_hash.hexdigest())

        return [return_code == 0, output, api, api_version]

    def test_split_by_class_sort(self):
        expected = [
            [
                'public class Api {',
                'public Api();',
                'public void a();',
                'public void b();',
                '}',
            ],
            [
                'public class zee {',
                'public abstract int z();',
                'public void x();',
                'public void y();',
                'public zee();',
                '}',
            ],
        ]
        input_str = """Compiled from Api.java
public class Api {
public void b();
public Api();
public void a();
}
Compiled from zee.java
public class zee {
public void x();
public zee();
public void y();
public abstract int z();
}
"""
        self.assertEqual(update_api._split_by_class(input_str.splitlines()),
                         expected)

    def test_update_api_success(self):
        # Test simple new API
        self.assertEqual(self.run_check_api_version('public void a(){}'), [
            True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'
        ])
        # Test version number not increased when API not changed
        self.assertEqual(self.run_check_api_version('public void a(){}'), [
            True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'
        ])
        # Test acceptable API method addition
        self.assertEqual(
            self.run_check_api_version('public void a(){} public void b(){}'),
            [
                True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
  public void b();
}
""", '2'
            ])
        # Test version number not increased when API not changed
        self.assertEqual(
            self.run_check_api_version('public void a(){} public void b(){}'),
            [
                True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
  public void b();
}
""", '2'
            ])
        # Test acceptable API class addition
        self.assertEqual(
            self.run_check_api_version(
                'public void a(){} public void b(){} public class C {}'),
            [
                True, '', CHECK_API_VERSION_PREFIX + """public class Api$C {
  public Api$C(Api);
}
public class Api {
  public Api();
  public void a();
  public void b();
}
""", '3'
            ])
        # Test version number not increased when API not changed
        self.assertEqual(
            self.run_check_api_version(
                'public void a(){} public void b(){} public class C {}'),
            [
                True, '', CHECK_API_VERSION_PREFIX + """public class Api$C {
  public Api$C(Api);
}
public class Api {
  public Api();
  public void a();
  public void b();
}
""", '3'
            ])

    def test_update_api_failure(self):
        # Create a simple new API
        self.assertEqual(self.run_check_api_version('public void a(){}'), [
            True, '', CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'
        ])
        # Test removing API method not allowed
        self.assertEqual(self.run_check_api_version(''), [
            False, ERROR_PREFIX_UPDATE_API + 'public void a();' +
            ERROR_SUFFIX_UPDATE_API,
            CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'
        ])
        # Test modifying API method not allowed
        self.assertEqual(self.run_check_api_version('public void a(int x){}'),
                         [
                             False, ERROR_PREFIX_UPDATE_API +
                             'public void a();' + ERROR_SUFFIX_UPDATE_API,
                             CHECK_API_VERSION_PREFIX + """public class Api {
  public Api();
  public void a();
}
""", '1'
                         ])
