import logging
import inspect
import subprocess
import sys
from unittest import mock

import pytest

from tools.wpt import browser


logger = logging.getLogger()


def test_all_browser_abc():
    # Make sure all subclasses of Browser implement all abstract methods
    # (except some known base classes). This is a basic sanity test in case
    # we change the ABC interface of Browser as we only instantiate some
    # products in unit tests.
    classes = inspect.getmembers(browser)
    for name, cls in classes:
        if cls in (browser.Browser, browser.ChromeAndroidBase):
            continue
        if inspect.isclass(cls) and issubclass(cls, browser.Browser):
            assert not inspect.isabstract(cls), "%s is abstract" % name


def test_edgechromium_webdriver_supports_browser():
    # EdgeDriver binary cannot be called.
    edge = browser.EdgeChromium(logger)
    edge.webdriver_version = mock.MagicMock(return_value=None)
    assert not edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge')

    # Browser binary cannot be called.
    edge = browser.EdgeChromium(logger)
    edge.webdriver_version = mock.MagicMock(return_value='70.0.1')
    edge.version = mock.MagicMock(return_value=None)
    assert edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge')

    # Browser version matches.
    edge = browser.EdgeChromium(logger)
    edge.webdriver_version = mock.MagicMock(return_value='70.0.1')
    edge.version = mock.MagicMock(return_value='70.1.5')
    assert edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge')

    # Browser version doesn't match.
    edge = browser.EdgeChromium(logger)
    edge.webdriver_version = mock.MagicMock(return_value='70.0.1')
    edge.version = mock.MagicMock(return_value='69.0.1')
    assert not edge.webdriver_supports_browser('/usr/bin/edgedriver', '/usr/bin/edge')


# On Windows, webdriver_version directly calls _get_fileversion, so there is no
# logic to test there.
@pytest.mark.skipif(sys.platform.startswith('win'), reason='just uses _get_fileversion on Windows')
@mock.patch('tools.wpt.browser.call')
def test_edgechromium_webdriver_version(mocked_call):
    edge = browser.EdgeChromium(logger)
    webdriver_binary = '/usr/bin/edgedriver'

    # Working cases.
    mocked_call.return_value = 'MSEdgeDriver 84.0.4147.30'
    assert edge.webdriver_version(webdriver_binary) == '84.0.4147.30'
    mocked_call.return_value = 'MSEdgeDriver 87.0.1 (abcd1234-refs/branch-heads/4147@{#310})'
    assert edge.webdriver_version(webdriver_binary) == '87.0.1'

    # Various invalid version strings
    mocked_call.return_value = 'Edge 84.0.4147.30 (dev)'
    assert edge.webdriver_version(webdriver_binary) is None
    mocked_call.return_value = 'MSEdgeDriver New 84.0.4147.30'
    assert edge.webdriver_version(webdriver_binary) is None
    mocked_call.return_value = ''
    assert edge.webdriver_version(webdriver_binary) is None

    # The underlying subprocess call throws.
    mocked_call.side_effect = subprocess.CalledProcessError(5, 'cmd', output='Call failed')
    assert edge.webdriver_version(webdriver_binary) is None


def test_chrome_webdriver_supports_browser():
    # ChromeDriver binary cannot be called.
    chrome = browser.Chrome(logger)
    chrome.webdriver_version = mock.MagicMock(return_value=None)
    assert not chrome.webdriver_supports_browser('/usr/bin/chromedriver', '/usr/bin/chrome', 'stable')

    # Browser binary cannot be called.
    chrome = browser.Chrome(logger)
    chrome.webdriver_version = mock.MagicMock(return_value='70.0.1')
    chrome.version = mock.MagicMock(return_value=None)
    assert chrome.webdriver_supports_browser('/usr/bin/chromedriver', '/usr/bin/chrome', 'stable')

    # Browser version matches.
    chrome = browser.Chrome(logger)
    chrome.webdriver_version = mock.MagicMock(return_value='70.0.1')
    chrome.version = mock.MagicMock(return_value='70.1.5')
    assert chrome.webdriver_supports_browser('/usr/bin/chromedriver', '/usr/bin/chrome', 'stable')

    # Browser version doesn't match.
    chrome = browser.Chrome(logger)
    chrome.webdriver_version = mock.MagicMock(return_value='70.0.1')
    chrome.version = mock.MagicMock(return_value='69.0.1')
    assert not chrome.webdriver_supports_browser('/usr/bin/chromedriver', '/usr/bin/chrome', 'stable')

    # The dev channel switches between beta and ToT ChromeDriver, so is sometimes
    # a version behind its ChromeDriver. As such, we accept browser version + 1 there.
    chrome = browser.Chrome(logger)
    chrome.webdriver_version = mock.MagicMock(return_value='70.0.1')
    chrome.version = mock.MagicMock(return_value='70.1.0')
    assert chrome.webdriver_supports_browser('/usr/bin/chromedriver', '/usr/bin/chrome', 'dev')
    chrome.webdriver_version = mock.MagicMock(return_value='71.0.1')
    assert chrome.webdriver_supports_browser('/usr/bin/chromedriver', '/usr/bin/chrome', 'dev')


# On Windows, webdriver_version directly calls _get_fileversion, so there is no
# logic to test there.
@pytest.mark.skipif(sys.platform.startswith('win'), reason='just uses _get_fileversion on Windows')
@mock.patch('tools.wpt.browser.call')
def test_chrome_webdriver_version(mocked_call):
    chrome = browser.Chrome(logger)
    webdriver_binary = '/usr/bin/chromedriver'

    # Working cases.
    mocked_call.return_value = 'ChromeDriver 84.0.4147.30'
    assert chrome.webdriver_version(webdriver_binary) == '84.0.4147.30'
    mocked_call.return_value = 'ChromeDriver 87.0.1 (abcd1234-refs/branch-heads/4147@{#310})'
    assert chrome.webdriver_version(webdriver_binary) == '87.0.1'

    # Various invalid version strings
    mocked_call.return_value = 'Chrome 84.0.4147.30 (dev)'
    assert chrome.webdriver_version(webdriver_binary) is None
    mocked_call.return_value = 'ChromeDriver New 84.0.4147.30'
    assert chrome.webdriver_version(webdriver_binary) is None
    mocked_call.return_value = ''
    assert chrome.webdriver_version(webdriver_binary) is None

    # The underlying subprocess call throws.
    mocked_call.side_effect = subprocess.CalledProcessError(5, 'cmd', output='Call failed')
    assert chrome.webdriver_version(webdriver_binary) is None


@mock.patch('subprocess.check_output')
def test_safari_version(mocked_check_output):
    safari = browser.Safari(logger)

    # Safari
    mocked_check_output.return_value = b'Included with Safari 12.1 (14607.1.11)'
    assert safari.version(webdriver_binary="safaridriver") == '12.1 (14607.1.11)'

    # Safari Technology Preview
    mocked_check_output.return_value = b'Included with Safari Technology Preview (Release 67, 13607.1.9.0.1)'
    assert safari.version(webdriver_binary="safaridriver") == 'Technology Preview (Release 67, 13607.1.9.0.1)'

@mock.patch('subprocess.check_output')
def test_safari_version_errors(mocked_check_output):
    safari = browser.Safari(logger)

    # No webdriver_binary
    assert safari.version() is None

    # `safaridriver --version` return gibberish
    mocked_check_output.return_value = b'gibberish'
    assert safari.version(webdriver_binary="safaridriver") is None

    # `safaridriver --version` fails (as it does for Safari <=12.0)
    mocked_check_output.return_value = b'dummy'
    mocked_check_output.side_effect = subprocess.CalledProcessError(1, 'cmd')
    assert safari.version(webdriver_binary="safaridriver") is None


@mock.patch('subprocess.check_output')
def test_webkitgtk_minibrowser_version(mocked_check_output):
    webkitgtk_minibrowser = browser.WebKitGTKMiniBrowser(logger)

    # stable version
    mocked_check_output.return_value = b'WebKitGTK 2.26.1\n'
    assert webkitgtk_minibrowser.version(binary='MiniBrowser') == '2.26.1'

    # nightly version
    mocked_check_output.return_value = b'WebKitGTK 2.27.1 (r250823)\n'
    assert webkitgtk_minibrowser.version(binary='MiniBrowser') == '2.27.1 (r250823)'

@mock.patch('subprocess.check_output')
def test_webkitgtk_minibrowser_version_errors(mocked_check_output):
    webkitgtk_minibrowser = browser.WebKitGTKMiniBrowser(logger)

    # No binary
    assert webkitgtk_minibrowser.version() is None

    # `MiniBrowser --version` return gibberish
    mocked_check_output.return_value = b'gibberish'
    assert webkitgtk_minibrowser.version(binary='MiniBrowser') is None

    # `MiniBrowser --version` fails (as it does for MiniBrowser <= 2.26.0)
    mocked_check_output.return_value = b'dummy'
    mocked_check_output.side_effect = subprocess.CalledProcessError(1, 'cmd')
    assert webkitgtk_minibrowser.version(binary='MiniBrowser') is None


# The test below doesn't work on Windows because distutils find_binary()
# on Windows only works if the binary name ends with a ".exe" suffix.
# But, WebKitGTK itself doesn't support Windows, so lets skip the test.
@pytest.mark.skipif(sys.platform.startswith('win'), reason='test not needed on Windows')
@mock.patch('os.path.isfile')
def test_webkitgtk_minibrowser_find_binary(mocked_os_path_isfile):
    webkitgtk_minibrowser = browser.WebKitGTKMiniBrowser(logger)

    # No MiniBrowser found
    mocked_os_path_isfile.side_effect = lambda path: path == '/etc/passwd'
    assert webkitgtk_minibrowser.find_binary() is None

    # Found on the default Fedora path
    fedora_minibrowser_path = '/usr/libexec/webkit2gtk-4.0/MiniBrowser'
    mocked_os_path_isfile.side_effect = lambda path: path == fedora_minibrowser_path
    assert webkitgtk_minibrowser.find_binary() == fedora_minibrowser_path

    # Found on the default Debian path for AMD64 (gcc not available)
    debian_minibrowser_path_amd64 = '/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/MiniBrowser'
    mocked_os_path_isfile.side_effect = lambda path: path == debian_minibrowser_path_amd64
    assert webkitgtk_minibrowser.find_binary() == debian_minibrowser_path_amd64

    # Found on the default Debian path for AMD64 (gcc available but gives an error)
    debian_minibrowser_path_amd64 = '/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/MiniBrowser'
    mocked_os_path_isfile.side_effect = lambda path: path in [debian_minibrowser_path_amd64, '/usr/bin/gcc']
    with mock.patch('subprocess.check_output', return_value = b'error', side_effect = subprocess.CalledProcessError(1, 'cmd')):
        assert webkitgtk_minibrowser.find_binary() == debian_minibrowser_path_amd64

        # Found on the default Debian path for ARM64 (gcc available)
        debian_minibrowser_path_arm64 = '/usr/lib/aarch64-linux-gnu/webkit2gtk-4.0/MiniBrowser'
        mocked_os_path_isfile.side_effect = lambda path: path in [debian_minibrowser_path_arm64, '/usr/bin/gcc']
        with mock.patch('subprocess.check_output', return_value = b'aarch64-linux-gnu'):
            assert webkitgtk_minibrowser.find_binary() == debian_minibrowser_path_arm64
