from __future__ import annotations

from pathlib import Path

import pytest
from pytest import approx
from shapely.geometry import Point, MultiPolygon

from game.flightplan.waypointstrategy import WaypointStrategy, angle_between_points
from game.utils import meters, Heading


def test_safe_prerequisite_safe_point() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    strategy.prerequisite(Point(0, 0)).is_safe()
    assert strategy.prerequisites_are_satisfied()


def test_safe_prerequisite_unsafe_point() -> None:
    strategy = WaypointStrategy(MultiPolygon([Point(0, 0).buffer(1)]))
    strategy.prerequisite(Point(0, 0)).is_safe()
    assert not strategy.prerequisites_are_satisfied()


def test_no_solution_if_prerequisites_failed() -> None:
    """Verify that no solution is found if prerequisites are not satisfied.

    This test has a 1-meter radius threat zone about the center of the plane. It has a
    prerequisite for a safe center, which will fail. The test verifies that even if
    there are no .require() constraints that would prevent finding a solution, failed
    prerequisites still prevent it (prerequisites differ from constraints in that they
    will prevent any of the other operations from happening without needing to location
    constraints, which is important because it allows strategies to avoid defending
    against invalid cases).
    """
    strategy = WaypointStrategy(MultiPolygon([Point(0, 0).buffer(1)]))
    strategy.prerequisite(Point(0, 0)).is_safe()
    # This constraint won't actually apply, but it's required before calling find() so
    # we need to set it even though it's not actually relevant to the test.
    strategy.nearest(Point(0, 0))
    assert strategy.find() is None


def test_has_solution_if_prerequisites_satisfied() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    strategy.prerequisite(Point(0, 0)).is_safe()
    strategy.nearest(Point(0, 0))
    assert strategy.find() is not None


def test_require_nearest() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    center = Point(0, 0)
    strategy.nearest(center)
    assert strategy.find() == center


def test_find_without_nearest_raises() -> None:
    with pytest.raises(RuntimeError):
        WaypointStrategy(MultiPolygon([])).find()


def test_multiple_nearest_raises() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    strategy.nearest(Point(0, 0))
    with pytest.raises(RuntimeError):
        strategy.nearest(Point(0, 0))


def METHOD_NAME() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    center = Point(0, 0)
    strategy.require().at_least(meters(10)).away_from(center)
    strategy.nearest(center)
    solution = strategy.find()
    assert solution is not None
    assert solution.distance(center) == approx(10, 0.1)


def test_require_at_most() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    center = Point(0, 0)
    strategy.require().at_most(meters(1)).away_from(center)
    strategy.nearest(Point(10, 0))
    solution = strategy.find()
    assert solution is not None
    assert solution.distance(center) <= 1


def test_require_safe() -> None:
    threat = MultiPolygon([Point(0, 0).buffer(10)])
    strategy = WaypointStrategy(threat)
    strategy.require().safe()
    strategy.nearest(Point(0, 0))
    solution = strategy.find()
    assert solution is not None
    assert not solution.intersects(threat)


def test_require_maximum_turn_to() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    turn_point = Point(1, 0)
    turn_target = Point(0, 0)
    strategy.require().maximum_turn_to(turn_point, turn_target, Heading(90))
    strategy.nearest(Point(0, 1))
    pre_turn_heading = Heading.from_degrees(
        angle_between_points(strategy.find(), turn_point)
    )
    post_turn_heading = Heading.from_degrees(
        angle_between_points(turn_point, turn_target)
    )
    assert pre_turn_heading.angle_between(post_turn_heading) <= Heading(90)


def test_combined_constraints() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    center = Point(0, 0)
    offset = Point(1, 0)
    midpoint = Point(0.5, 0)
    strategy.require().at_least(meters(1)).away_from(center)
    strategy.require().at_least(meters(1)).away_from(offset)
    strategy.nearest(midpoint)
    solution = strategy.find()
    assert solution is not None
    assert solution.distance(center) == approx(1, rel=0.1, abs=0.1)
    assert solution.distance(offset) == approx(1, rel=0.1, abs=0.1)
    assert solution.distance(midpoint) < 1


def test_threat_tolerance(tmp_path: Path) -> None:
    home = Point(20, 0)
    target = Point(-1, 0)
    max_distance = meters(5)
    threat = MultiPolygon([Point(0, 0).buffer(10)])
    strategy = WaypointStrategy(threat)
    strategy.require().at_most(max_distance).away_from(target)
    strategy.threat_tolerance(target, max_distance, meters(1))
    strategy.require().safe()
    strategy.nearest(home)
    solution = strategy.find()
    assert solution is not None
    # Max distance of 5 from -1, so the point should be at 4. Home is at 20.
    assert solution.distance(home) == 16


def test_threat_tolerance_does_nothing_if_no_threats(tmp_path: Path) -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    strategy.threat_tolerance(Point(0, 0), meters(1), meters(1))
    assert strategy._threat_tolerance is None


def test_no_solutions() -> None:
    strategy = WaypointStrategy(MultiPolygon([]))
    strategy.require().at_most(meters(1)).away_from(Point(0, 0))
    strategy.require().at_least(meters(2)).away_from(Point(0, 0))
    strategy.nearest(Point(0, 0))
    assert strategy.find() is None


def test_debug() -> None:
    center = Point(0, 0)
    threat = MultiPolygon([center.buffer(5)])
    strategy = WaypointStrategy(threat)
    strategy.require().at_most(meters(10)).away_from(center, "center")
    strategy.require().at_least(meters(2)).away_from(center)
    strategy.require().safe()
    strategy.nearest(center)
    solution = strategy.find()
    assert solution is not None
    debug_info = list(strategy.iter_debug_info())
    assert len(debug_info) == 4
    max_distance_debug, min_distance_debug, safe_debug, solution_debug = debug_info
    assert max_distance_debug.description == "at most 10 meters away from center"
    assert max_distance_debug.geometry.distance(center) == approx(10, 0.1)
    assert min_distance_debug.description == "at least 2 meters away from POINT (0 0)"
    assert max_distance_debug.geometry.boundary.distance(center) == approx(10, 0.1)
    assert safe_debug.description == "safe"
    assert safe_debug.geometry == threat
    assert solution_debug.description == "solution"
    assert solution_debug.geometry == solution


def test_debug_info_omits_solution_if_none() -> None:
    center = Point(0, 0)
    strategy = WaypointStrategy(MultiPolygon([]))
    strategy.require().at_most(meters(1)).away_from(center)
    strategy.require().at_least(meters(2)).away_from(center)
    strategy.nearest(center)
    debug_infos = list(strategy.iter_debug_info())
    assert len(debug_infos) == 2