import contextlib
import time
from unittest.mock import MagicMock, patch

import pytest
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait

from promium.base import Element
from promium.exceptions import PromiumException, PromiumTimeout
from promium.expected_conditions import is_present
from promium.waits import (
    wait,
    wait_for_alert,
    wait_for_status_code,
    wait_until,
    wait_until_new_window_is_opened,
    wait_until_not,
    wait_url_contains,
)


FAKE_LOCATOR = ("", "")


@pytest.fixture
def driver():
    return MagicMock(
        name="driver",
        **{
            "refresh.return_value": "refresh OK",
            "session_id": "666",
            "window_handles": ["old_window", "new_window_0", "new_window_1"],
            "current_url": "http://www.example.com/testing",
        },
    )


def return_alert(status=None):
    return MagicMock(
        name="driver",
        **{
            "switch_to.alert": status,
        },
    )


@pytest.mark.unit
def test_base_wait(driver):
    w = wait(driver)
    assert isinstance(w, WebDriverWait)
    assert hasattr(w, "until")
    assert hasattr(w, "until_not")


@pytest.mark.unit
@patch.object(Element, "_find_elements", return_value=True)
@pytest.mark.parametrize(
    ("seconds", "msg"), [(0.01, None), (0, ""), (0.2, "test error message")]
)
def test_wait_until(mock_element, seconds, msg, driver):
    element = Element(*FAKE_LOCATOR)
    element.driver = driver
    start_time = time.time()
    wait_until(driver, is_present(element), seconds, msg)
    end_time = int(time.time() - start_time)
    mock_element.assert_called_once()
    assert end_time == 0, "Not correct wait time duration"


@pytest.mark.unit
@patch.object(Element, "_find_elements", return_value=False)
@pytest.mark.parametrize(
    ("seconds", "msg", "ex_result"),
    [(1, None, (1 * 2) + 1), (0, "", 1), (5, "test error message", (5 * 2) + 1)],
)
def test_wait_until_negative(mock_element, seconds, msg, ex_result, driver):
    element = Element(*FAKE_LOCATOR)
    element.driver = driver
    start_time = time.time()
    with pytest.raises(PromiumTimeout):
        wait_until(driver, is_present(element), seconds, msg)
    end_time = int(time.time() - start_time)
    mock_element.assert_called()
    assert mock_element.call_count == ex_result
    assert end_time == seconds, "Not correct wait time duration"


@pytest.mark.unit
@pytest.mark.parametrize(
    ("var", "ex", "seconds"),
    [
        ([True], 1, 0),
        ([False, True], 2, 0),
        ([False, False, True], 3, 1),
        ([False] * 4 + [True], 5, 2),
    ],
)
def test_wait_until_count(var, ex, seconds, driver):
    with patch.object(Element, "_find_elements", side_effect=var) as el:
        element = Element(*FAKE_LOCATOR)
        element.driver = driver
        start_time = time.time()
        wait_until(driver, is_present(element))
        end_time = int(time.time() - start_time)
        el.assert_called()
        assert el.call_count == ex
        assert end_time == seconds, "Not correct wait time duration"


@pytest.mark.unit
@pytest.mark.parametrize(
    ("var", "ex", "seconds"),
    [
        ([False], 1, 0),
        ([True, False], 2, 0),
        ([True, True, False], 3, 1),
        ([True] * 4 + [False], 5, 2),
    ],
)
def test_wait_until_not_positive(var, ex, seconds, driver):
    with patch.object(Element, "_find_elements", side_effect=var) as el:
        element = Element(*FAKE_LOCATOR)
        element.driver = driver
        start_time = time.time()
        wait_until_not(driver, is_present(element))
        end_time = int(time.time() - start_time)
        el.assert_called()
        assert el.call_count == ex
        assert end_time == seconds, "Not correct wait time duration"


@pytest.mark.unit
@patch.object(Element, "_find_elements", return_value=True)
@pytest.mark.parametrize(("ex", "seconds"), [(1, 0), (3, 1), (11, 5)])
def test_wait_until_not_negative(mock_element, ex, seconds, driver):
    element = Element(*FAKE_LOCATOR)
    element.driver = driver
    start_time = time.time()
    with pytest.raises(PromiumException):
        wait_until_not(driver, is_present(element), seconds)
    end_time = int(time.time() - start_time)
    mock_element.assert_called()
    assert mock_element.call_count == ex
    assert end_time == seconds, "Not correct wait time duration"


@pytest.mark.unit
@pytest.mark.parametrize(
    ("var", "ex", "ex_sec"),
    [
        ([True], 1, 0),
        ([False, True], 2, 0),
        ([False, False, True], 3, 1),
        ([False, False] * 3 + [True], 7, 3),
    ],
)
def test_wait_until_with_reload(var, ex, ex_sec, driver):
    with patch.object(Element, "_find_elements", side_effect=var) as el:
        element = Element(*FAKE_LOCATOR)
        element.driver = driver
        start_time = time.time()
        wait_until(
            driver=driver,
            expression=is_present(element, with_refresh=True),
            seconds=10,
        )
        end_time = int(time.time() - start_time)
        el.assert_called()
        assert el.call_count == ex
        assert end_time == ex_sec, "Not correct wait time duration"
        driver.refresh.assert_called()
        assert driver.refresh.call_count == ex


@pytest.mark.unit
@patch.object(Element, "_find_elements", return_value=False)
@pytest.mark.parametrize(("ex", "sec"), [(1, 0), (3, 1), (11, 5)])
def test_wait_until_with_reload_negative(mock_element, ex, sec, driver):
    element = Element(*FAKE_LOCATOR)
    element.driver = driver
    start_time = time.time()
    with pytest.raises(PromiumTimeout):
        wait_until(
            driver=driver,
            expression=is_present(element, with_refresh=True),
            seconds=sec,
        )
    end_time = int(time.time() - start_time)
    mock_element.assert_called()
    assert end_time == sec, "Not correct wait time duration"
    driver.refresh.assert_called()
    assert driver.refresh.call_count == ex


@pytest.mark.unit
def test_wait_for_alert():
    start_time = time.time()
    wait_status = wait_for_alert(driver=return_alert(True), seconds=1)
    end_time = int(time.time() - start_time)
    assert wait_status is True, "Not correct wait return status"
    assert end_time < 1, "Not correct wait time duration"


@pytest.mark.unit
@pytest.mark.parametrize("sec", [0, 2, 3])
def test_wait_for_alert_negative(sec):
    start_time = time.time()
    with pytest.raises(TimeoutException):
        wait_for_alert(driver=return_alert(False), seconds=sec)
    end_time = int(time.time() - start_time)
    assert end_time == sec, "Not correct wait time duration"


@pytest.mark.unit
@pytest.mark.parametrize("tries", [1, 2, 10, 15])
def test_wait_for_status_code(tries):
    start_time = time.time()
    wait_for_status_code("http://google.com/page?id=666", 404, tries)
    end_time = int(time.time() - start_time)
    assert end_time == 0


@pytest.mark.unit
@pytest.mark.parametrize("tries", [1, 3])
def test_wait_for_status_code_negative(tries):
    start_time = time.time()
    with pytest.raises(PromiumException):
        wait_for_status_code("http://google.com/page?id=666", 200, tries)
    end_time = int(time.time() - start_time)
    assert end_time == tries


@pytest.mark.unit
@pytest.mark.parametrize(
    ("index", "window_handle"),
    [
        (0, "new_window_0"),
        (1, "new_window_1"),
    ],
)
def test_wait_until_new_window_is_opened(index, window_handle, driver):
    element = Element(*FAKE_LOCATOR)
    element.driver = driver
    new_window = wait_until_new_window_is_opened(driver, "old_window", index)
    assert new_window == window_handle


@pytest.mark.unit
def test_wait_until_new_window_is_opened_negative(driver):
    element = Element(*FAKE_LOCATOR)
    element.driver = driver
    with contextlib.suppress(ValueError):
        wait_until_new_window_is_opened(driver, "some_unreal_window_name")


@pytest.mark.unit
def test_wait_url_contains1(driver):
    element = Element(*FAKE_LOCATOR)
    element.driver = driver
    start_time = time.time()
    wait_url_contains(driver, "example.com", timeout=1)
    end_time = int(time.time() - start_time)
    assert end_time == 0, "Not correct wait time duration"


@pytest.mark.unit
@pytest.mark.parametrize("sec", [1, 2, 4])
def test_wait_url_contains_negative(driver, sec):
    element = Element(*FAKE_LOCATOR)
    element.driver = driver
    start_time = time.time()
    with pytest.raises(PromiumTimeout):
        wait_url_contains(driver, "uncorrect url", timeout=sec)
    end_time = int(time.time() - start_time)
    assert end_time == sec, "Not correct wait time duration"
