import pytest

from webdriver.error import InvalidArgumentException, NoSuchWindowException, StaleElementReferenceException

from tests.classic.perform_actions.support.mouse import (
    get_inview_center,
    get_viewport_rect,
)
from tests.classic.perform_actions.support.refine import get_events
from tests.support.asserts import assert_move_to_coordinates
from tests.support.helpers import filter_dict
from tests.support.sync import Poll

from . import assert_pointer_events, record_pointer_events


def test_null_response_value(session, mouse_chain):
    value = mouse_chain.click().perform()
    assert value is None


def test_no_top_browsing_context(session, closed_window, mouse_chain):
    with pytest.raises(NoSuchWindowException):
        mouse_chain.click().perform()


def test_no_browsing_context(session, closed_frame, mouse_chain):
    with pytest.raises(NoSuchWindowException):
        mouse_chain.click().perform()


@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
def test_stale_element_reference(session, stale_element, mouse_chain, as_frame):
    element = stale_element("input#text", as_frame=as_frame)

    with pytest.raises(StaleElementReferenceException):
        mouse_chain.click(element=element).perform()


def test_click_at_coordinates(session, test_actions_page, mouse_chain):
    div_point = {
        "x": 82,
        "y": 187,
    }
    mouse_chain \
        .pointer_move(div_point["x"], div_point["y"], duration=1000) \
        .click() \
        .perform()
    events = get_events(session)
    assert len(events) == 4
    assert_move_to_coordinates(div_point, "outer", events)
    for e in events:
        if e["type"] != "mousedown":
            assert e["buttons"] == 0
        assert e["button"] == 0
    expected = [
        {"type": "mousedown", "buttons": 1},
        {"type": "mouseup", "buttons": 0},
        {"type": "click", "buttons": 0},
    ]
    filtered_events = [filter_dict(e, expected[0]) for e in events]
    assert expected == filtered_events[1:]


def test_context_menu_at_coordinates(session, test_actions_page, mouse_chain):
    div_point = {
        "x": 82,
        "y": 187,
    }
    mouse_chain \
        .pointer_move(div_point["x"], div_point["y"]) \
        .pointer_down(button=2) \
        .pointer_up(button=2) \
        .perform()

    events = get_events(session)
    assert len(events) == 4

    expected = [
        {"type": "mousedown", "button": 2, "buttons": 2},
        {"type": "contextmenu", "button": 2, "buttons": 2},
    ]
    # Some browsers in some platforms may dispatch `contextmenu` event as a
    # a default action of `mouseup`.  In the case, `.buttons` of the event
    # should be 0.
    anotherExpected = [
        {"type": "mousedown", "button": 2, "buttons": 2},
        {"type": "contextmenu", "button": 2, "buttons": 0},
    ]
    filtered_events = [filter_dict(e, expected[0]) for e in events]
    mousedown_contextmenu_events = [
        x for x in filtered_events
        if x["type"] in ["mousedown", "contextmenu"]
    ]
    assert mousedown_contextmenu_events in [expected, anotherExpected]


def test_middle_click(session, test_actions_page, mouse_chain):
    div_point = {
        "x": 82,
        "y": 187,
    }
    mouse_chain \
        .pointer_move(div_point["x"], div_point["y"]) \
        .pointer_down(button=1) \
        .pointer_up(button=1) \
        .perform()

    events = get_events(session)
    assert len(events) == 3

    expected = [
        {"type": "mousedown", "button": 1, "buttons": 4},
        {"type": "mouseup", "button": 1, "buttons": 0},
    ]
    filtered_events = [filter_dict(e, expected[0]) for e in events]
    mousedown_mouseup_events = [
        x for x in filtered_events
        if x["type"] in ["mousedown", "mouseup"]
    ]
    assert expected == mousedown_mouseup_events


def test_click_element_center(session, test_actions_page, mouse_chain):
    outer = session.find.css("#outer", all=False)
    center = get_inview_center(outer.rect, get_viewport_rect(session))
    mouse_chain.click(element=outer).perform()
    events = get_events(session)
    assert len(events) == 4
    event_types = [e["type"] for e in events]
    assert ["mousemove", "mousedown", "mouseup", "click"] == event_types
    for e in events:
        if e["type"] != "mousemove":
            assert e["pageX"] == pytest.approx(center["x"], abs=1.0)
            assert e["pageY"] == pytest.approx(center["y"], abs=1.0)
            assert e["target"] == "outer"


@pytest.mark.parametrize("mode", ["open", "closed"])
@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
def test_click_element_in_shadow_tree(
    session, get_test_page, mouse_chain, mode, nested
):
    session.url = get_test_page(
        shadow_doc="""
        <div id="pointer-target"
             style="width: 10px; height: 10px; background-color:blue;">
        </div>""",
        shadow_root_mode=mode,
        nested_shadow_dom=nested,
    )

    shadow_root = session.find.css("custom-element", all=False).shadow_root
    if nested:
        shadow_root = shadow_root.find_element("css selector", "inner-custom-element").shadow_root

    target = shadow_root.find_element("css selector", "#pointer-target")
    record_pointer_events(session, target)

    mouse_chain.click(element=target).perform()
    assert_pointer_events(
        session,
        expected_events=["pointerdown", "pointerup"],
        target="pointer-target",
        pointer_type="mouse",
    )


def test_click_navigation(session, url, inline):
    destination = url("/webdriver/tests/support/html/test_actions.html")
    start = inline("<a href=\"{}\" id=\"link\">destination</a>".format(destination))

    def click(link):
        mouse_chain = session.actions.sequence(
            "pointer", "pointer_id", {"pointerType": "mouse"})
        mouse_chain.click(element=link).perform()

    session.url = start
    error_message = "Did not navigate to %s" % destination

    click(session.find.css("#link", all=False))
    Poll(session, message=error_message).until(lambda s: s.url == destination)
    # repeat steps to check behaviour after document unload
    session.url = start
    click(session.find.css("#link", all=False))
    Poll(session, message=error_message).until(lambda s: s.url == destination)


@pytest.mark.parametrize("x, y, event_count", [
    (0, 0, 0),
    (1, 0, 1),
    (0, 1, 1),
], ids=["default value", "x", "y"])
def test_move_to_position_in_viewport(
    session, test_actions_page, mouse_chain, x, y, event_count
):
    mouse_chain.pointer_move(x, y).perform()
    events = get_events(session)
    assert len(events) == event_count

    # Move again to check that no further mouse move event is emitted.
    mouse_chain.pointer_move(x, y).perform()
    events = get_events(session)
    assert len(events) == event_count


@pytest.mark.parametrize("drag_duration", [0, 300, 800])
@pytest.mark.parametrize("dx, dy", [
    (20, 0), (0, 15), (10, 15), (-20, 0), (10, -15), (-10, -15)
])
def test_drag_and_drop(session,
                       test_actions_page,
                       mouse_chain,
                       dx,
                       dy,
                       drag_duration):
    drag_target = session.find.css("#dragTarget", all=False)
    initial_rect = drag_target.rect
    initial_center = get_inview_center(initial_rect, get_viewport_rect(session))
    # Conclude chain with extra move to allow time for last queued
    # coordinate-update of drag_target and to test that drag_target is "dropped".
    mouse_chain \
        .pointer_move(0, 0, origin=drag_target) \
        .pointer_down() \
        .pointer_move(dx, dy, duration=drag_duration, origin="pointer") \
        .pointer_up() \
        .pointer_move(80, 50, duration=100, origin="pointer") \
        .perform()
    # mouseup that ends the drag is at the expected destination
    e = get_events(session)[1]
    assert e["type"] == "mouseup"
    assert e["pageX"] == pytest.approx(initial_center["x"] + dx, abs=1.0)
    assert e["pageY"] == pytest.approx(initial_center["y"] + dy, abs=1.0)
    # check resulting location of the dragged element
    final_rect = drag_target.rect
    assert initial_rect["x"] + dx == final_rect["x"]
    assert initial_rect["y"] + dy == final_rect["y"]


@pytest.mark.parametrize("drag_duration", [0, 300, 800])
def test_drag_and_drop_with_draggable_element(session_new_window,
                       test_actions_page,
                       mouse_chain,
                       drag_duration):
    new_session = session_new_window
    drag_target = new_session.find.css("#draggable", all=False)
    drop_target = new_session.find.css("#droppable", all=False)
    # Conclude chain with extra move to allow time for last queued
    # coordinate-update of drag_target and to test that drag_target is "dropped".
    mouse_chain \
        .pointer_move(0, 0, origin=drag_target) \
        .pointer_down() \
        .pointer_move(50,
                      25,
                      duration=drag_duration,
                      origin=drop_target) \
        .pointer_up() \
        .pointer_move(80, 50, duration=100, origin="pointer") \
        .perform()
    # mouseup that ends the drag is at the expected destination
    e = get_events(new_session)
    assert len(e) >= 5
    assert e[1]["type"] == "dragstart", "Events captured were {}".format(e)
    assert e[2]["type"] == "dragover", "Events captured were {}".format(e)
    drag_events_captured = [
        ev["type"] for ev in e if ev["type"].startswith("drag") or ev["type"].startswith("drop")
    ]
    assert "dragend" in drag_events_captured
    assert "dragenter" in drag_events_captured
    assert "dragleave" in drag_events_captured
    assert "drop" in drag_events_captured


@pytest.mark.parametrize("missing", ["x", "y"])
def test_missing_coordinates(session, test_actions_page, mouse_chain, missing):
    outer = session.find.css("#outer", all=False)
    actions = mouse_chain.pointer_move(x=0, y=0, origin=outer)
    del actions._actions[-1][missing]
    with pytest.raises(InvalidArgumentException):
        actions.perform()


def test_invalid_element_origin(session, test_actions_page, mouse_chain):
    outer = session.find.css("#outer", all=False)
    actions = mouse_chain.pointer_move(
        x=0, y=0, origin={"type": "element", "element": {"sharedId": outer.id}}
    )
    with pytest.raises(InvalidArgumentException):
        actions.perform()
