# Copyright (C) 2019-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.

"""Tests for here.inspector common functions."""
from dataclasses import fields, is_dataclass
from typing import Dict, List, Tuple, Union
from unittest.mock import Mock, call

import pytest
from geojson import Point
from here.inspector import Color, Features, Inspector, Theme, inspect, new_inspector, options


def test_new_inspector():
    i = new_inspector()
    assert isinstance(i, Inspector)


def test_options():
    """Test if standard options contain default names."""
    default_option_names = {"default_colors", "default_theme", "api_key", "inspector_class"}
    assert is_dataclass(options)
    assert default_option_names.issubset({f.name for f in fields(options)})


@pytest.fixture()
def mocked_inspector() -> Mock:
    inspector = Mock(spec=Inspector)
    inspector.show.return_value = "Show!"
    assert isinstance(inspector, Inspector)
    assert isinstance(inspector, Mock)
    return inspector


def test_inspect_empty(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    r = inspect()
    assert r == "Show!"

    mocked_inspector.add_features.assert_not_called()
    mocked_inspector.add_tiles.assert_not_called()
    mocked_inspector.set_center.assert_not_called()
    mocked_inspector.set_zoom.assert_not_called()
    mocked_inspector.set_theme.assert_not_called()
    mocked_inspector.show.assert_called_once()


def test_inspect_features(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    features = [Point((0, 0)), Point((1, 1))]
    r = inspect(features=features, name="f", style=Color.RED)
    assert r == "Show!"

    mocked_inspector.add_features.assert_called_once_with(
        features=features, name="f", style=Color.RED
    )
    mocked_inspector.add_tiles.assert_not_called()
    mocked_inspector.set_center.assert_not_called()
    mocked_inspector.set_zoom.assert_not_called()
    mocked_inspector.set_theme.assert_not_called()
    mocked_inspector.show.assert_called_once()


def test_inspect_mapping_layers(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    points_a = [Point((0, 0)), Point((1, 1))]
    points_b = [Point((-1, 0)), Point((0, 1))]
    points_c = [Point((-2, +2))]
    layers = {"A": points_a, "B": points_b, "C": points_c}
    r = inspect(layers=layers)
    assert r == "Show!"

    assert mocked_inspector.add_features.call_args_list == [
        call(features=points_a, name="A", style=None),
        call(features=points_b, name="B", style=None),
        call(features=points_c, name="C", style=None),
    ]
    mocked_inspector.add_tiles.assert_not_called()
    mocked_inspector.set_center.assert_not_called()
    mocked_inspector.set_zoom.assert_not_called()
    mocked_inspector.set_theme.assert_not_called()
    mocked_inspector.show.assert_called_once()


def test_inspect_mapping_layers_styles(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    points_a = [Point((0, 0)), Point((1, 1))]
    points_b = [Point((-1, 0)), Point((0, 1))]
    points_c = [Point((-2, +2))]
    layers = {"A": points_a, "B": points_b, "C": points_c}
    # the type hint sometimes is required as mypy can't infer it properly
    layers_style: Dict[str, Union[Color, Dict]] = {"A": Color.RED, "C": {"my": "style"}}
    r = inspect(layers=layers, layers_style=layers_style)
    assert r == "Show!"

    assert mocked_inspector.add_features.call_args_list == [
        call(features=points_a, name="A", style=Color.RED),
        call(features=points_b, name="B", style=None),
        call(features=points_c, name="C", style={"my": "style"}),
    ]
    mocked_inspector.add_tiles.assert_not_called()
    mocked_inspector.set_center.assert_not_called()
    mocked_inspector.set_zoom.assert_not_called()
    mocked_inspector.set_theme.assert_not_called()
    mocked_inspector.show.assert_called_once()


def test_inspect_sequence_layers(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    points_a = [Point((0, 0)), Point((1, 1))]
    points_b = [Point((-1, 0)), Point((0, 1))]
    points_c = [Point((-2, +2))]
    # the type hint sometimes is required as mypy can't infer it properly
    layers: List[
        Union[Tuple[str, Features], Tuple[str, Features, Color], Tuple[str, Features, Dict]]
    ] = [
        ("A", points_a, Color.RED),
        ("B", points_b),
        ("C", points_c, {"my": "style"}),
    ]
    r = inspect(layers=layers)
    assert r == "Show!"

    assert mocked_inspector.add_features.call_args_list == [
        call(features=points_a, name="A", style=Color.RED),
        call(features=points_b, name="B", style=None),
        call(features=points_c, name="C", style={"my": "style"}),
    ]
    mocked_inspector.add_tiles.assert_not_called()
    mocked_inspector.set_center.assert_not_called()
    mocked_inspector.set_zoom.assert_not_called()
    mocked_inspector.set_theme.assert_not_called()
    mocked_inspector.show.assert_called_once()


def test_inspect_sequence_exceptions(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    points_a = [Point((0, 0)), Point((1, 1))]
    points_b = [Point((-1, 0)), Point((0, 1))]
    layers = [("A", points_a, Color.RED), ("B", points_b), "whatever"]

    with pytest.raises(ValueError):
        inspect(layers=layers)  # type: ignore

    # two calls should be successful
    assert len(mocked_inspector.add_features.call_args_list) == 2

    with pytest.raises(ValueError):
        inspect(layers="whatever")  # type: ignore

    # no additional calls
    assert len(mocked_inspector.add_features.call_args_list) == 2


def test_inspect_tiles(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    r = inspect(tiles=[1, 4, 5, 6, 7], tiles_style=Color.RED)
    assert r == "Show!"

    mocked_inspector.add_features.assert_not_called()
    mocked_inspector.add_tiles.assert_called_once_with(tiles=[1, 4, 5, 6, 7], style=Color.RED)
    mocked_inspector.set_center.assert_not_called()
    mocked_inspector.set_zoom.assert_not_called()
    mocked_inspector.set_theme.assert_not_called()
    mocked_inspector.show.assert_called_once()


def test_inspect_map_params(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    r = inspect(center=Point((0, 1)), zoom=3, theme=Theme.DARK_MAP)
    assert r == "Show!"

    mocked_inspector.add_features.assert_not_called()
    mocked_inspector.add_tiles.assert_not_called()
    mocked_inspector.set_center.assert_called_once_with(center=Point((0, 1)))
    mocked_inspector.set_zoom.assert_called_once_with(zoom=3)
    mocked_inspector.set_theme.assert_called_once_with(theme=Theme.DARK_MAP)
    mocked_inspector.show.assert_called_once()


def test_inspect_mix(mocker, mocked_inspector: Mock):
    mocker.patch("here.inspector.new_inspector", return_value=mocked_inspector)

    points_a = [Point((0, 0)), Point((1, 1))]
    points_b = [Point((-1, 0)), Point((0, 1))]
    points_c = [Point((-2, +2))]
    layers = {"A": points_a, "B": points_b, "C": points_c}
    # the type hint sometimes is required as mypy can't infer it properly
    layers_style: Dict[str, Union[Color, Dict]] = {"A": Color.RED, "C": {"my": "style"}}
    r = inspect(
        layers=layers,
        layers_style=layers_style,
        style=Color.BLUE,
        tiles=[1, 4, 5, 6, 7],
        tiles_style=Color.RED,
        center=Point((0, 1)),
        zoom=3,
        theme=Theme.DARK_MAP,
    )
    assert r == "Show!"

    assert mocked_inspector.add_features.call_args_list == [
        call(features=points_a, name="A", style=Color.RED),
        call(features=points_b, name="B", style=None),
        call(features=points_c, name="C", style={"my": "style"}),
    ]
    mocked_inspector.add_tiles.assert_called_once_with(tiles=[1, 4, 5, 6, 7], style=Color.RED)
    mocked_inspector.set_center.assert_called_once_with(center=Point((0, 1)))
    mocked_inspector.set_zoom.assert_called_once_with(zoom=3)
    mocked_inspector.set_theme.assert_called_once_with(theme=Theme.DARK_MAP)
    mocked_inspector.show.assert_called_once()
