;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL

(ns common-tests.geom-shapes-intersect-test
  (:require
   [app.common.geom.point :as gpt]
   [app.common.geom.rect :as grc]
   [app.common.geom.shapes.intersect :as gint]
   [app.common.math :as mth]
   [clojure.test :as t]))

(defn- pt [x y] (gpt/point x y))

;; ---- orientation ----

(t/deftest orientation-test
  (t/testing "Counter-clockwise orientation"
    (t/is (= ::gint/counter-clockwise (gint/orientation (pt 0 0) (pt 1 0) (pt 1 1)))))

  (t/testing "Clockwise orientation"
    (t/is (= ::gint/clockwise (gint/orientation (pt 0 0) (pt 1 1) (pt 1 0)))))

  (t/testing "Collinear points"
    (t/is (= ::gint/coplanar (gint/orientation (pt 0 0) (pt 1 1) (pt 2 2))))))

;; ---- on-segment? ----

(t/deftest on-segment?-test
  (t/testing "Point on segment"
    (t/is (true? (gint/on-segment? (pt 5 5) (pt 0 0) (pt 10 10)))))

  (t/testing "Point not on segment"
    (t/is (false? (gint/on-segment? (pt 5 10) (pt 0 0) (pt 10 0)))))

  (t/testing "Point at endpoint"
    (t/is (true? (gint/on-segment? (pt 0 0) (pt 0 0) (pt 10 10))))))

;; ---- intersect-segments? ----

(t/deftest intersect-segments?-test
  (t/testing "Two crossing segments"
    (t/is (true? (gint/intersect-segments?
                  [(pt 0 0) (pt 10 10)]
                  [(pt 10 0) (pt 0 10)]))))

  (t/testing "Two parallel non-intersecting segments"
    (t/is (false? (gint/intersect-segments?
                   [(pt 0 0) (pt 10 0)]
                   [(pt 0 5) (pt 10 5)]))))

  (t/testing "Two collinear overlapping segments"
    ;; Collinear overlapping segments correctly detected as intersecting.
    (t/is (true? (gint/intersect-segments?
                  [(pt 0 0) (pt 10 0)]
                  [(pt 5 0) (pt 15 0)]))))

  (t/testing "Two non-overlapping collinear segments"
    (t/is (false? (gint/intersect-segments?
                   [(pt 0 0) (pt 5 0)]
                   [(pt 10 0) (pt 15 0)]))))

  (t/testing "Segments sharing an endpoint"
    (t/is (true? (gint/intersect-segments?
                  [(pt 0 0) (pt 5 5)]
                  [(pt 5 5) (pt 10 0)])))))

;; ---- points->lines ----

(t/deftest points->lines-test
  (t/testing "Triangle produces 3 closed lines"
    (let [points [(pt 0 0) (pt 10 0) (pt 5 10)]
          lines  (gint/points->lines points)]
      (t/is (= 3 (count lines)))))

  (t/testing "Square produces 4 closed lines"
    (let [points [(pt 0 0) (pt 10 0) (pt 10 10) (pt 0 10)]
          lines  (gint/points->lines points)]
      (t/is (= 4 (count lines)))))

  (t/testing "Open polygon (not closed)"
    (let [points [(pt 0 0) (pt 10 0) (pt 10 10)]
          lines  (gint/points->lines points false)]
      (t/is (= 2 (count lines))))))

;; ---- intersect-ray? ----

(t/deftest intersect-ray?-test
  (t/testing "Ray from right intersects segment that crosses y to the left"
    ;; Point at (5, 5), ray goes right (+x). Vertical segment at x=10 crosses y=[0,10].
    ;; Since x=10 > x=5, and the segment goes from below y=5 to above y=5, it intersects.
    (let [point (pt 5 5)
          segment [(pt 10 0) (pt 10 10)]]
      (t/is (true? (gint/intersect-ray? point segment)))))

  (t/testing "Ray does not intersect segment to the left of point"
    ;; Vertical segment at x=0 is to the LEFT of point (5,5).
    ;; Ray goes right, so no intersection.
    (let [point (pt 5 5)
          segment [(pt 0 0) (pt 0 10)]]
      (t/is (false? (gint/intersect-ray? point segment)))))

  (t/testing "Ray does not intersect horizontal segment"
    ;; Horizontal segment at y=0 doesn't cross y=5
    (let [point (pt 5 5)
          segment [(pt 0 0) (pt 10 0)]]
      (t/is (false? (gint/intersect-ray? point segment))))))

;; ---- is-point-inside-evenodd? ----

(t/deftest is-point-inside-evenodd?-test
  (let [square-lines (gint/points->lines [(pt 0 0) (pt 10 0) (pt 10 10) (pt 0 10)])]
    (t/testing "Point inside square"
      (t/is (true? (gint/is-point-inside-evenodd? (pt 5 5) square-lines))))

    (t/testing "Point outside square"
      (t/is (false? (gint/is-point-inside-evenodd? (pt 15 15) square-lines))))

    (t/testing "Point on edge (edge case)"
      (t/is (boolean? (gint/is-point-inside-evenodd? (pt 0 5) square-lines))))))

;; ---- is-point-inside-nonzero? ----

(t/deftest is-point-inside-nonzero?-test
  (let [square-lines (gint/points->lines [(pt 0 0) (pt 10 0) (pt 10 10) (pt 0 10)])]
    (t/testing "Point inside square"
      (t/is (true? (gint/is-point-inside-nonzero? (pt 5 5) square-lines))))

    (t/testing "Point outside square"
      (t/is (false? (gint/is-point-inside-nonzero? (pt 15 15) square-lines))))))

;; ---- overlaps-rect-points? ----

(t/deftest overlaps-rect-points?-test
  (t/testing "Overlapping rects"
    (let [rect   (grc/make-rect 0 0 10 10)
          points (grc/rect->points (grc/make-rect 5 5 10 10))]
      (t/is (true? (gint/overlaps-rect-points? rect points)))))

  (t/testing "Non-overlapping rects"
    (let [rect   (grc/make-rect 0 0 10 10)
          points (grc/rect->points (grc/make-rect 20 20 10 10))]
      (t/is (false? (gint/overlaps-rect-points? rect points)))))

  (t/testing "One rect inside another"
    (let [rect   (grc/make-rect 0 0 100 100)
          points (grc/rect->points (grc/make-rect 10 10 20 20))]
      (t/is (true? (gint/overlaps-rect-points? rect points))))))

;; ---- is-point-inside-ellipse? ----

(t/deftest is-point-inside-ellipse?-test
  (let [ellipse {:cx 50 :cy 50 :rx 25 :ry 15}]
    (t/testing "Center is inside"
      (t/is (true? (gint/is-point-inside-ellipse? (pt 50 50) ellipse))))

    (t/testing "Point on boundary"
      (t/is (true? (gint/is-point-inside-ellipse? (pt 75 50) ellipse))))

    (t/testing "Point outside"
      (t/is (false? (gint/is-point-inside-ellipse? (pt 100 50) ellipse))))

    (t/testing "Point on minor axis boundary"
      (t/is (true? (gint/is-point-inside-ellipse? (pt 50 65) ellipse))))))

;; ---- line-line-intersect ----

(t/deftest line-line-intersect-test
  (t/testing "Intersection of crossing lines"
    (let [result (gint/line-line-intersect (pt 0 0) (pt 10 10) (pt 10 0) (pt 0 10))]
      (t/is (gpt/point? result))
      (t/is (mth/close? 5.0 (:x result)))
      (t/is (mth/close? 5.0 (:y result)))))

  (t/testing "Intersection of horizontal and vertical lines"
    (let [result (gint/line-line-intersect (pt 0 5) (pt 10 5) (pt 5 0) (pt 5 10))]
      (t/is (gpt/point? result))
      (t/is (mth/close? 5.0 (:x result)))
      (t/is (mth/close? 5.0 (:y result)))))

  (t/testing "Near-parallel lines still produce a point"
    (let [result (gint/line-line-intersect (pt 0 0) (pt 10 0) (pt 0 0.001) (pt 10 0.001))]
      (t/is (gpt/point? result)))))

;; ---- has-point-rect? ----

(t/deftest has-point-rect?-test
  (t/testing "Point inside rect"
    (t/is (true? (gint/has-point-rect? (grc/make-rect 0 0 100 100) (pt 50 50)))))

  (t/testing "Point outside rect"
    (t/is (false? (gint/has-point-rect? (grc/make-rect 0 0 100 100) (pt 150 50)))))

  (t/testing "Point at corner"
    (t/is (true? (gint/has-point-rect? (grc/make-rect 0 0 100 100) (pt 0 0))))))

;; ---- rect-contains-shape? ----

(t/deftest rect-contains-shape?-test
  (t/testing "Rect contains all shape points"
    (let [shape {:points [(pt 10 10) (pt 20 10) (pt 20 20) (pt 10 20)]}
          rect  (grc/make-rect 0 0 100 100)]
      (t/is (true? (gint/rect-contains-shape? rect shape)))))

  (t/testing "Rect does not contain all shape points"
    (let [shape {:points [(pt 10 10) (pt 200 10) (pt 200 200) (pt 10 200)]}
          rect  (grc/make-rect 0 0 100 100)]
      (t/is (false? (gint/rect-contains-shape? rect shape))))))

;; ---- intersects-lines? ----

(t/deftest intersects-lines?-test
  (t/testing "Intersecting line sets"
    (let [lines-a (gint/points->lines [(pt 0 0) (pt 10 10)])
          lines-b (gint/points->lines [(pt 10 0) (pt 0 10)])]
      (t/is (true? (gint/intersects-lines? lines-a lines-b)))))

  (t/testing "Non-intersecting line sets"
    (let [lines-a (gint/points->lines [(pt 0 0) (pt 10 0)])
          lines-b (gint/points->lines [(pt 0 10) (pt 10 10)])]
      (t/is (false? (gint/intersects-lines? lines-a lines-b)))))

  (t/testing "Empty line sets"
    (t/is (false? (gint/intersects-lines? [] [])))))

;; ---- intersects-line-ellipse? ----

(t/deftest intersects-line-ellipse?-test
  (let [ellipse {:cx 50 :cy 50 :rx 25 :ry 25}]
    (t/testing "Line passing through ellipse"
      (t/is (some? (gint/intersects-line-ellipse? [(pt 0 50) (pt 100 50)] ellipse))))

    (t/testing "Line not touching ellipse"
      (t/is (nil? (gint/intersects-line-ellipse? [(pt 0 0) (pt 10 0)] ellipse))))

    (t/testing "Line tangent to ellipse"
      (t/is (some? (gint/intersects-line-ellipse? [(pt 75 0) (pt 75 100)] ellipse))))))

;; ---- fast-has-point? / slow-has-point? ----

(t/deftest has-point-tests
  (t/testing "fast-has-point? inside shape"
    (let [shape {:x 10 :y 20 :width 100 :height 50}]
      (t/is (true? (gint/fast-has-point? shape (pt 50 40))))))

  (t/testing "fast-has-point? outside shape"
    (let [shape {:x 10 :y 20 :width 100 :height 50}]
      (t/is (false? (gint/fast-has-point? shape (pt 200 40))))))

  (t/testing "slow-has-point? with axis-aligned shape"
    (let [points [(pt 0 0) (pt 100 0) (pt 100 50) (pt 0 50)]
          shape  {:points points}]
      (t/is (true? (gint/slow-has-point? shape (pt 50 25))))
      (t/is (false? (gint/slow-has-point? shape (pt 150 25)))))))

(t/deftest has-point-rotated-test
  ;; Diamond (a square rotated 45º); its axis-aligned x/y/width/height box does
  ;; not match the rotated polygon.
  (let [points [(pt 50 0) (pt 100 50) (pt 50 100) (pt 0 50)]
        shape  {:x 20 :y 20 :width 60 :height 60 :rotation 45 :points points}]
    (t/testing "point inside the polygon but outside the box is contained"
      (t/is (true? (gint/has-point? shape (pt 50 5)))))
    (t/testing "point inside the box but outside the polygon is not contained"
      (t/is (false? (gint/has-point? shape (pt 22 22)))))))

(t/deftest has-point-axis-aligned-test
  (let [shape {:x 10 :y 20 :width 100 :height 50 :rotation 0}]
    (t/testing "unrotated shape uses the axis-aligned box"
      (t/is (true? (gint/has-point? shape (pt 50 40))))
      (t/is (false? (gint/has-point? shape (pt 200 40)))))))
