//  Copyright (c) 2022 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 		http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package searcher

import (
	"context"
	"reflect"
	"testing"

	"github.com/blevesearch/bleve/v2/document"
	"github.com/blevesearch/bleve/v2/geo"
	"github.com/blevesearch/bleve/v2/index/scorch"
	"github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap"
	"github.com/blevesearch/bleve/v2/search"
	index "github.com/blevesearch/bleve_index_api"
)

func TestGeoJsonPointContainsQuery(t *testing.T) {
	tests := []struct {
		point []float64
		field string
		want  []string
	}{
		// test points inside the polygon1.
		{
			[]float64{77.58334636688232, 12.948268838994263},
			"geometry",
			[]string{"polygon1"},
		},

		// test points inside the circle1.
		{
			[]float64{77.58553504943848, 12.954040501528555},
			"geometry",
			[]string{"circle1"},
		},

		// test points inside the polygon1 and the circle.
		{
			[]float64{77.59293794631958, 12.948896200093982},
			"geometry",
			[]string{"polygon1", "circle1"},
		},

		// test points outside the polygon1 and the circle1.
		{
			[]float64{77.5614595413208, 12.953287683563568},
			"geometry", nil,
		},

		// test point within the envelope1.
		{
			[]float64{81.28166198730469, 26.34203746601541},
			"geometry",
			[]string{"envelope1"},
		},

		// test point on the linestring vertex.
		{
			[]float64{77.57776737213135, 12.952074805390097},
			"geometry",
			[]string{"linestring1"},
		},

		// test point on the multilinestring vertex.
		{
			[]float64{77.5779390335083, 12.945006535817749},
			"geometry",
			[]string{"multilinestring1"},
		},

		// test point on the multipoint vertex.
		{
			[]float64{77.56407737731932, 12.951614746607163},
			"geometry",
			[]string{"multipoint1"},
		},

		// test point within the polygonWithHole1.
		{
			[]float64{77.60334491729736, 12.979844051951334},
			"geometry",
			[]string{"polygonWithHole1"},
		},

		// test point within the hole of the polygonWithHole1.
		{
			[]float64{77.60244369506836, 12.976247607394027},
			"geometry", nil,
		},
	}
	i := setupGeoJsonShapesIndex(t)
	indexReader, err := i.Reader()
	if err != nil {
		t.Error(err)
	}
	defer func() {
		err = indexReader.Close()
		if err != nil {
			t.Fatal(err)
		}
	}()

	for n, test := range tests {
		got, err := runGeoShapePointRelationQuery("contains",
			false, indexReader, [][]float64{test.point}, test.field)
		if err != nil {
			t.Fatal(err)
		}
		if !reflect.DeepEqual(got, test.want) {
			t.Errorf("test %d, expected %v, got %v for polygon: %+v",
				n, test.want, got, test.point)
		}
	}
}

func TestGeoJsonMultiPointWithInQuery(t *testing.T) {
	tests := []struct {
		multipoint [][]float64
		field      string
		want       []string
	}{
		// test multipoint inside the polygon1.
		{
			[][]float64{
				{77.58334636688232, 12.948268838994263},
				{77.58467674255371, 12.944295515355652},
			},
			"geometry",
			[]string{"polygon1"},
		},

		// test multipoint inside the circle1.
		{
			[][]float64{
				{77.58553504943848, 12.954040501528555},
				{77.58643627166747, 12.956089827794571},
			},
			"geometry",
			[]string{"circle1"},
		},

		// test multipoint inside the envelope1.
		{
			[][]float64{
				{81.28166198730469, 26.34203746601541},
				{80.94314575195312, 26.346960121309415},
			},
			"geometry",
			[]string{"envelope1"},
		},

		// test multipoint inside the polygon1 and the circle.
		{
			[][]float64{
				{77.59293794631958, 12.948896200093982},
				{77.58532047271729, 12.953789562459688},
			},
			"geometry",
			[]string{"polygon1", "circle1"},
		},

		// test multipoint (only 1 point outside) outside.
		{[][]float64{
			{77.58334636688232, 12.948268838994263},
			{77.58643627166747, 12.956089827794571},
			{77.5615, 12.9533},
		}, "geometry", nil},

		// test multipoint on the linestring vertex.
		{
			[][]float64{
				{77.5841188430786, 12.957093573282744},
				{77.57776737213135, 12.952074805390097},
			},
			"geometry",
			[]string{"linestring1"},
		},

		// test multipoint outside the linestring vertex.
		{
			[][]float64{
				{77.5841188430786, 12.957093573282744},
				{77.57776737213135, 12.952074805390097},
				{77.58334636688232, 12.948268838994263},
			},
			"geometry", nil,
		},

		// test multipoint on the multilinestring vertex.
		{
			[][]float64{
				{77.5779390335083, 12.94471376293191},
				{77.57218837738037, 12.948268838994263},
			},
			"geometry",
			[]string{"multilinestring1"},
		},

		// test multipoint outside the multilinestring vertex.
		{
			[][]float64{
				{77.5779390335083, 12.94471376293191},
				{77.57218837738037, 12.948268838994263},
				{77.58532047271729, 12.953789562459688},
			},
			"geometry", nil,
		},

		// test multipoint with one inside the hole within the polygonWithHole1.
		{
			[][]float64{
				{77.60334491729736, 12.979844051951334},
				{77.60244369506836, 12.976247607394027},
			},
			"geometry", nil,
		},

		// test multipoint with all inside the hole within the polygonWithHole1.
		{
			[][]float64{
				{77.59656429290771, 12.981767710239714},
				{77.59888172149658, 12.979969508380469},
			},
			"geometry", nil,
		},

		// test multipoint with all inside the polygonWithHole1.
		{
			[][]float64{
				{77.60334491729736, 12.979844051951334},
				{77.59656429290771, 12.981767710239714},
				{77.59802341461182, 12.9751602999608},
			},
			"geometry",
			[]string{"polygonWithHole1"},
		},
	}
	i := setupGeoJsonShapesIndex(t)
	indexReader, err := i.Reader()
	if err != nil {
		t.Error(err)
	}
	defer func() {
		err = indexReader.Close()
		if err != nil {
			t.Fatal(err)
		}
	}()

	for n, test := range tests {
		got, err := runGeoShapePointRelationQuery("contains",
			true, indexReader, test.multipoint, test.field)
		if err != nil {
			t.Fatal(err)
		}
		if !reflect.DeepEqual(got, test.want) {
			t.Errorf("test %d, expected %v, got %v for polygon: %+v",
				n, test.want, got, test.multipoint)
		}
	}
}

func TestGeoJsonMultiPointIntersectsQuery(t *testing.T) {
	tests := []struct {
		multipoint [][]float64
		field      string
		want       []string
	}{
		// test multipoint inside the polygon1.
		{
			[][]float64{
				{77.58334636688232, 12.948268838994263},
				{77.58467674255371, 12.944295515355652},
			},
			"geometry",
			[]string{"polygon1"},
		},

		// test multipoint inside the circle1.
		{
			[][]float64{
				{77.58553504943848, 12.954040501528555},
				{77.58643627166747, 12.956089827794571},
			},
			"geometry",
			[]string{"circle1"},
		},

		// test multipoint inside the envelope1. (1 point outside)
		{
			[][]float64{
				{81.28166198730469, 26.34203746601541},
				{80.94314575195312, 26.346960121309415},
				{81.12716674804688, 26.353728430338332},
			},
			"geometry",
			[]string{"envelope1"},
		},

		// test multipoint inside the polygon1 and the circle.
		{
			[][]float64{
				{77.59293794631958, 12.948896200093982},
				{77.58532047271729, 12.953789562459688},
			},
			"geometry",
			[]string{"polygon1", "circle1"},
		},

		// test multipoint (only 1 point outside) intersects.
		{
			[][]float64{
				{77.58334636688232, 12.948268838994263},
				{77.58643627166747, 12.956089827794571},
				{77.5615, 12.9533},
			},
			"geometry",
			[]string{"polygon1", "circle1"},
		},

		// test multipoint on the linestring vertex.
		{
			[][]float64{
				{77.5841188430786, 12.957093573282744},
				{77.57776737213135, 12.952074805390097},
			},
			"geometry",
			[]string{"linestring1"},
		},

		// test multipoint outside the linestring vertex.
		{
			[][]float64{
				{77.5841188430786, 12.957093573282744},
				{77.57776737213135, 12.952074805390097},
				{77.58334636688232, 12.948268838994263},
			},
			"geometry",
			[]string{"polygon1", "linestring1"},
		},

		// test multipoint on the multilinestring vertex.
		{
			[][]float64{
				{77.5779390335083, 12.94471376293191},
				{77.57218837738037, 12.948268838994263},
			},
			"geometry",
			[]string{"multilinestring1"},
		},

		// test multipoint outside the multilinestring vertex.
		{
			[][]float64{
				{77.5779390335083, 12.94471376293191},
				{77.57218837738037, 12.948268838994263},
				{77.58532047271729, 12.953789562459688},
			},
			"geometry",
			[]string{"polygon1", "circle1", "multilinestring1"},
		},

		// test multipoint with one inside the hole within the polygonWithHole1.
		{
			[][]float64{
				{77.60334491729736, 12.979844051951334},
				{77.60244369506836, 12.976247607394027},
			},
			"geometry",
			[]string{"polygonWithHole1"},
		},

		// test multipoint with all inside the hole within the polygonWithHole1.
		{
			[][]float64{
				{77.60244369506836, 12.976247607394027},
				{77.59888172149658, 12.979969508380469},
			},
			"geometry", nil,
		},

		// test multipoint with all inside the polygonWithHole1.
		{
			[][]float64{
				{77.60334491729736, 12.979844051951334},
				{77.59656429290771, 12.981767710239714},
				{77.59802341461182, 12.9751602999608},
			},
			"geometry",
			[]string{"polygonWithHole1"},
		},
	}
	i := setupGeoJsonShapesIndex(t)
	indexReader, err := i.Reader()
	if err != nil {
		t.Error(err)
	}
	defer func() {
		err = indexReader.Close()
		if err != nil {
			t.Fatal(err)
		}
	}()

	for n, test := range tests {
		got, err := runGeoShapePointRelationQuery("intersects",
			true, indexReader, test.multipoint, test.field)
		if err != nil {
			t.Fatal(err)
		}
		if !reflect.DeepEqual(got, test.want) {
			t.Errorf("test %d, expected %v, got %v for polygon: %+v",
				n, test.want, got, test.multipoint)
		}
	}
}

func runGeoShapePointRelationQuery(relation string, multi bool,
	i index.IndexReader, points [][]float64, field string,
) ([]string, error) {
	var rv []string
	var s index.GeoJSON
	if multi {
		s = geo.NewGeoJsonMultiPoint(points)
	} else {
		s = geo.NewGeoJsonPoint(points[0])
	}

	gbs, err := NewGeoShapeSearcher(context.TODO(), i, s, relation, field, 1.0, search.SearcherOptions{})
	if err != nil {
		return nil, err
	}
	ctx := &search.SearchContext{
		DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0),
	}
	docMatch, err := gbs.Next(ctx)
	for docMatch != nil && err == nil {
		docID, _ := i.ExternalID(docMatch.IndexInternalID)
		rv = append(rv, docID)
		docMatch, err = gbs.Next(ctx)
	}
	if err != nil {
		return nil, err
	}
	return rv, nil
}

type Fatalfable interface {
	Fatalf(format string, args ...interface{})
}

func setupGeoJsonShapesIndex(t *testing.T) index.Index {
	analysisQueue := index.NewAnalysisQueue(1)
	i, err := scorch.NewScorch(
		gtreap.Name,
		map[string]interface{}{
			"path":          "",
			"spatialPlugin": "s2",
		},
		analysisQueue)
	if err != nil {
		t.Fatal(err)
	}
	err = i.Open()
	if err != nil {
		t.Fatal(err)
	}

	polygon1 := [][][][]float64{{{
		{77.5853419303894, 12.953977766785052},
		{77.58405447006226, 12.95393594361393},
		{77.5819730758667, 12.9495026476557},
		{77.58068561553955, 12.94883346405509},
		{77.58019208908081, 12.948331575175299},
		{77.57991313934326, 12.943814529775414},
		{77.58497714996338, 12.94394000436408},
		{77.58517026901245, 12.9446301134728},
		{77.58572816848755, 12.945508431393435},
		{77.58785247802734, 12.946365833997325},
		{77.58967638015747, 12.946428570657417},
		{77.59070634841918, 12.947474179333993},
		{77.59317398071289, 12.948875288082773},
		{77.59167194366454, 12.949962710338657},
		{77.59077072143555, 12.950276388953625},
		{77.59098529815674, 12.951196510612728},
		{77.58729457855225, 12.952472128200755},
		{77.5853419303894, 12.953977766785052},
	}}}
	doc := document.NewDocument("polygon1")
	doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{},
		polygon1, "polygon", document.DefaultGeoShapeIndexingOptions))
	err = i.Update(doc)
	if err != nil {
		t.Fatal(err)
	}

	// not working envelope
	envelope1 := [][][][]float64{{{
		{80.93696594238281, 26.33957605983274},
		{81.28440856933594, 26.351267272877074},
	}}}
	doc = document.NewDocument("envelope1")
	doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{},
		envelope1, "envelope", document.DefaultGeoShapeIndexingOptions))
	err = i.Update(doc)
	if err != nil {
		t.Fatal(err)
	}

	doc = document.NewDocument("circle1")
	doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", []uint64{},
		[]float64{77.59137153625487, 12.952660333521468}, "900m",
		document.DefaultGeoShapeIndexingOptions))
	err = i.Update(doc)
	if err != nil {
		t.Fatal(err)
	}

	linestring := [][][][]float64{{{
		{77.5841188430786, 12.957093573282744},
		{77.57776737213135, 12.952074805390097},
	}}}
	doc = document.NewDocument("linestring1")
	doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{},
		linestring, "linestring", document.DefaultGeoShapeIndexingOptions))
	err = i.Update(doc)
	if err != nil {
		t.Fatal(err)
	}

	multilinestring := [][][][]float64{{{
		{77.57227420806883, 12.948687079902895},
		{77.57600784301758, 12.954165970968194},
		{77.5779390335083, 12.94471376293191},
		{77.57218837738037, 12.948268838994263},
		{77.57781028747559, 12.951740217268595},
		{77.5779390335083, 12.945006535817749},
	}}}
	doc = document.NewDocument("multilinestring1")
	doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{},
		multilinestring, "multilinestring", document.DefaultGeoShapeIndexingOptions))
	err = i.Update(doc)
	if err != nil {
		t.Fatal(err)
	}

	multipoint1 := [][][][]float64{{{
		{77.56618022918701, 12.958180959662695},
		{77.56407737731932, 12.951614746607163},
		{77.56922721862793, 12.956173473406446},
	}}}
	doc = document.NewDocument("multipoint1")
	doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{},
		multipoint1, "multipoint", document.DefaultGeoShapeIndexingOptions))
	err = i.Update(doc)
	if err != nil {
		t.Fatal(err)
	}

	polygonWithHole1 := [][][][]float64{{
		{
			{77.59991168975829, 12.972232910164502},
			{77.6039457321167, 12.97582941279006},
			{77.60424613952637, 12.98168407323241},
			{77.59974002838135, 12.985489528568463},
			{77.59321689605713, 12.979300406693417},
			{77.59991168975829, 12.972232910164502},
		},
		{
			{77.59682178497314, 12.975787593290978},
			{77.60295867919922, 12.975787593290978},
			{77.60295867919922, 12.98143316204164},
			{77.59682178497314, 12.98143316204164},
			{77.59682178497314, 12.975787593290978},
		},
	}}

	doc = document.NewDocument("polygonWithHole1")
	doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{},
		polygonWithHole1, "polygon", document.DefaultGeoShapeIndexingOptions))
	err = i.Update(doc)
	if err != nil {
		t.Fatal(err)
	}

	return i
}
