//  Copyright (c) 2026 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 bleve

import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/blevesearch/bleve/v2/mapping"
	"github.com/blevesearch/bleve/v2/search"
	"github.com/blevesearch/bleve/v2/search/highlight/highlighter/ansi"
	"github.com/blevesearch/bleve/v2/search/query"
)

func createNestedIndexMapping() mapping.IndexMapping {
	/*
		company
		├── id
		├── name
		├── departments[] (nested)
		│     ├── name
		│     ├── budget
		│     ├── employees[] (nested)
		│     │     ├── name
		│     │     ├── role
		│     └── projects[] (nested)
		│           ├── title
		│           ├── status
		└── locations[] (nested)
				├── city
				├── country
	*/

	// Create the index mapping
	imap := mapping.NewIndexMapping()

	// Create company mapping
	companyMapping := mapping.NewDocumentMapping()

	// Company ID field
	companyIDField := mapping.NewTextFieldMapping()
	companyMapping.AddFieldMappingsAt("id", companyIDField)

	// Company name field
	companyNameField := mapping.NewTextFieldMapping()
	companyMapping.AddFieldMappingsAt("name", companyNameField)

	// Departments mapping
	departmentsMapping := mapping.NewNestedDocumentMapping()

	// Department name field
	deptNameField := mapping.NewTextFieldMapping()
	departmentsMapping.AddFieldMappingsAt("name", deptNameField)

	// Department budget field
	deptBudgetField := mapping.NewNumericFieldMapping()
	departmentsMapping.AddFieldMappingsAt("budget", deptBudgetField)

	// Employees mapping
	employeesMapping := mapping.NewNestedDocumentMapping()

	// Employee name field
	empNameField := mapping.NewTextFieldMapping()
	employeesMapping.AddFieldMappingsAt("name", empNameField)

	// Employee role field
	empRoleField := mapping.NewTextFieldMapping()
	employeesMapping.AddFieldMappingsAt("role", empRoleField)

	departmentsMapping.AddSubDocumentMapping("employees", employeesMapping)

	// Projects mapping
	projectsMapping := mapping.NewNestedDocumentMapping()

	// Project title field
	projTitleField := mapping.NewTextFieldMapping()
	projectsMapping.AddFieldMappingsAt("title", projTitleField)

	// Project status field
	projStatusField := mapping.NewTextFieldMapping()
	projectsMapping.AddFieldMappingsAt("status", projStatusField)

	departmentsMapping.AddSubDocumentMapping("projects", projectsMapping)

	companyMapping.AddSubDocumentMapping("departments", departmentsMapping)

	// Locations mapping
	locationsMapping := mapping.NewNestedDocumentMapping()

	// Location city field
	cityField := mapping.NewTextFieldMapping()
	locationsMapping.AddFieldMappingsAt("city", cityField)

	// Location country field
	countryField := mapping.NewTextFieldMapping()
	locationsMapping.AddFieldMappingsAt("country", countryField)

	companyMapping.AddSubDocumentMapping("locations", locationsMapping)

	// Add company to type mapping
	imap.DefaultMapping.AddSubDocumentMapping("company", companyMapping)

	return imap
}

func TestNestedPrefixes(t *testing.T) {
	imap := createNestedIndexMapping()

	tmpIndexPath := createTmpIndexPath(t)
	defer cleanupTmpIndexPath(t, tmpIndexPath)

	idx, err := New(tmpIndexPath, imap)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := idx.Close(); err != nil {
			t.Fatal(err)
		}
	}()

	nmap, ok := imap.(mapping.NestedMapping)
	if !ok {
		t.Fatal("index mapping is not a NestedMapping")
	}

	// ----------------------------------------------------------------------
	// Test 1: Employee Role AND Employee Name
	// ----------------------------------------------------------------------
	fs := search.NewFieldSet()
	fs.AddField("company.departments.employees.role")
	fs.AddField("company.departments.employees.name")

	expectedCommon := 2
	expectedMax := 2

	common, max := nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test1: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 2: Employee Role AND Employee Name AND Department Name
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()
	fs.AddField("company.departments.employees.role")
	fs.AddField("company.departments.employees.name")
	fs.AddField("company.departments.name")

	expectedCommon = 1
	expectedMax = 2 // employees nested deeper

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test2: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 3: Employee Role AND Location City
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()
	fs.AddField("company.departments.employees.role")
	fs.AddField("company.locations.city")

	expectedCommon = 0
	expectedMax = 2 // employees deeper than locations (1)

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test3: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 4: Company Name AND Location Country
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()
	fs.AddField("company.name")
	fs.AddField("company.locations.country")
	fs.AddField("company.locations.city")

	expectedCommon = 0
	expectedMax = 1 // locations.country and locations.city share depth 1

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test4: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 5: Department Budget AND Project Status AND Employee Name
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()
	fs.AddField("company.departments.budget")
	fs.AddField("company.departments.projects.status")
	fs.AddField("company.departments.employees.name")

	expectedCommon = 1
	expectedMax = 2 // employees + projects go deeper

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test5: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 6: Single Field
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()
	fs.AddField("company.id")

	expectedCommon = 0
	expectedMax = 0

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test6: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 7: No Fields
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()

	expectedCommon = 0
	expectedMax = 0

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test7: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 8: All Fields
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()
	fs.AddField("company.id")
	fs.AddField("company.name")
	fs.AddField("company.departments.name")
	fs.AddField("company.departments.budget")
	fs.AddField("company.departments.employees.name")
	fs.AddField("company.departments.employees.role")
	fs.AddField("company.departments.projects.title")
	fs.AddField("company.departments.projects.status")
	fs.AddField("company.locations.city")
	fs.AddField("company.locations.country")

	expectedCommon = 0 // spans different contexts
	expectedMax = 2

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test8: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 9: Project Title AND Project Status
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()
	fs.AddField("company.departments.projects.title")
	fs.AddField("company.departments.projects.status")

	expectedCommon = 2
	expectedMax = 2

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test9: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}

	// ----------------------------------------------------------------------
	// Test 10: Department Name AND Location Country
	// ----------------------------------------------------------------------
	fs = search.NewFieldSet()
	fs.AddField("company.departments.name")
	fs.AddField("company.locations.country")
	fs.AddField("company.locations.city")

	expectedCommon = 0
	expectedMax = 1 // locations share depth 1

	common, max = nmap.NestedDepth(fs)
	if common != expectedCommon || max != expectedMax {
		t.Fatalf("Test10: expected (common=%d, max=%d), got (common=%d, max=%d)",
			expectedCommon, expectedMax, common, max)
	}
}

func TestNestedConjunctionQuery(t *testing.T) {
	imap := createNestedIndexMapping()
	err := imap.Validate()
	if err != nil {
		t.Fatalf("expected valid nested index mapping, got error: %v", err)
	}
	tmpIndexPath := createTmpIndexPath(t)
	defer cleanupTmpIndexPath(t, tmpIndexPath)
	idx, err := New(tmpIndexPath, imap)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		err = idx.Close()
		if err != nil {
			t.Fatal(err)
		}
	}()
	// Index 3 sample documents
	docs := []struct {
		id   string
		data string
	}{
		{
			id: "doc1",
			data: `{
				"company": {
					"id": "c1",
					"name": "TechCorp",
					"departments": [
						{
							"name": "Engineering",
							"budget": 2000000,
							"employees": [
								{"name": "Alice", "role": "Engineer"},
								{"name": "Bob", "role": "Manager"}
							],
							"projects": [
								{"title": "Project X", "status": "ongoing"},
								{"title": "Project Y", "status": "completed"}
							]
						},
						{
							"name": "Sales",
							"budget": 300000,
							"employees": [
								{"name": "Eve", "role": "Salesperson"},
								{"name": "Mallory", "role": "Manager"}
							],
							"projects": [
								{"title": "Project A", "status": "completed"},
								{"title": "Project B", "status": "ongoing"}
							]
						}	
					],
					"locations": [
						{"city": "Athens", "country": "Greece"},
						{"city": "Berlin", "country": "USA"}
					]
				}
			}`,
		},
		{
			id: "doc2",
			data: `{
				"company" : {
					"id": "c2",
					"name": "BizInc",
					"departments": [
						{
							"name": "Marketing",
							"budget": 800000,
							"employees": [
								{"name": "Eve", "role": "Marketer"},
								{"name": "David", "role": "Manager"}
							],
							"projects": [
								{"title": "Project Z", "status": "ongoing"},
								{"title": "Project W", "status": "planned"}
							]
						},
						{
							"name": "Engineering",
							"budget": 800000,
							"employees": [
								{"name": "Frank", "role": "Manager"},
								{"name": "Grace", "role": "Engineer"}
							],
							"projects": [
								{"title": "Project Alpha", "status": "completed"},
								{"title": "Project Beta", "status": "ongoing"}
							]
						}	
					],
					"locations": [
						{"city": "Athens", "country": "USA"},
						{"city": "London", "country": "UK"}
					]
				}
			}`,
		},
		{
			id: "doc3",
			data: `{
				"company": {
					"id": "c3",
					"name": "WebSolutions",
					"departments": [
						{
							"name": "HR",
							"budget": 800000,
							"employees": [
								{"name": "Eve", "role": "Manager"},
								{"name": "Frank", "role": "HR"}
							],
							"projects": [
								{"title": "Project Beta", "status": "completed"},
								{"title": "Project B", "status": "ongoing"}
							]
						},
						{
							"name": "Engineering",
							"budget": 200000,
							"employees": [
								{"name": "Heidi", "role": "Support Engineer"},
								{"name": "Ivan", "role": "Manager"}
							],
							"projects": [
								{"title": "Project Helpdesk", "status": "ongoing"},
								{"title": "Project FAQ", "status": "completed"}
							]
						}
					],
					"locations": [
						{"city": "Edinburgh", "country": "UK"},
						{"city": "London", "country": "Canada"}
					]
				}
			}`,
		},
	}

	for _, doc := range docs {
		var dataMap map[string]interface{}
		err := json.Unmarshal([]byte(doc.data), &dataMap)
		if err != nil {
			t.Fatalf("failed to unmarshal document %s: %v", doc.id, err)
		}
		err = idx.Index(doc.id, dataMap)
		if err != nil {
			t.Fatalf("failed to index document %s: %v", doc.id, err)
		}
	}

	var buildReq = func(subQueries []query.Query) *SearchRequest {
		rv := NewSearchRequest(query.NewConjunctionQuery(subQueries))
		rv.SortBy([]string{"_id"})
		rv.Fields = []string{"*"}
		rv.Highlight = NewHighlightWithStyle(ansi.Name)
		return rv
	}

	var (
		req             *SearchRequest
		res             *SearchResult
		deptNameQuery   *query.MatchQuery
		deptBudgetQuery *query.NumericRangeQuery
		empNameQuery    *query.MatchQuery
		empRoleQuery    *query.MatchQuery
		projTitleQuery  *query.MatchPhraseQuery
		projStatusQuery *query.MatchQuery
		countryQuery    *query.MatchQuery
		cityQuery       *query.MatchQuery
	)

	// Test 1: Find companies with a department named "Engineering" AND budget over 900000
	deptNameQuery = query.NewMatchQuery("Engineering")
	deptNameQuery.SetField("company.departments.name")

	min := float64(800000)
	deptBudgetQuery = query.NewNumericRangeQuery(&min, nil)
	deptBudgetQuery.SetField("company.departments.budget")

	req = buildReq([]query.Query{deptNameQuery, deptBudgetQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 2 {
		t.Fatalf("expected 2 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc1" || res.Hits[1].ID != "doc2" {
		t.Fatalf("unexpected hit IDs: %v, %v", res.Hits[0].ID, res.Hits[1].ID)
	}

	// Test 2: Find companies with an employee named "Eve" AND project status "completed"
	empNameQuery = query.NewMatchQuery("Eve")
	empNameQuery.SetField("company.departments.employees.name")

	projStatusQuery = query.NewMatchQuery("completed")
	projStatusQuery.SetField("company.departments.projects.status")

	req = buildReq([]query.Query{empNameQuery, projStatusQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 2 {
		t.Fatalf("expected 2 hits, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc1" || res.Hits[1].ID != "doc3" {
		t.Fatalf("unexpected hit IDs: %v, %v", res.Hits[0].ID, res.Hits[1].ID)
	}

	// Test 3: Find companies located in "Athens, USA" AND with an Engineering department
	countryQuery = query.NewMatchQuery("USA")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("Athens")
	cityQuery.SetField("company.locations.city")

	locQuery := query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	deptNameQuery = query.NewMatchQuery("Engineering")
	deptNameQuery.SetField("company.departments.name")

	req = buildReq([]query.Query{locQuery, deptNameQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc2" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	// Test 4a: Find companies located in "Athens, USA" AND with an Engineering department with a budget over 1M
	countryQuery = query.NewMatchQuery("USA")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("Athens")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	deptNameQuery = query.NewMatchQuery("Engineering")
	deptNameQuery.SetField("company.departments.name")

	min = float64(1000000)
	deptBudgetQuery = query.NewNumericRangeQuery(&min, nil)
	deptBudgetQuery.SetField("company.departments.budget")

	deptQuery := query.NewConjunctionQuery([]query.Query{deptNameQuery, deptBudgetQuery})

	req = buildReq([]query.Query{locQuery, deptQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hits, got %d", len(res.Hits))
	}

	// Test 4b: Find companies located in "Athens, Greece" AND with an Engineering department with a budget over 1M
	countryQuery = query.NewMatchQuery("Greece")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("Athens")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	deptNameQuery = query.NewMatchQuery("Engineering")
	deptNameQuery.SetField("company.departments.name")

	min = float64(1000000)
	deptBudgetQuery = query.NewNumericRangeQuery(&min, nil)
	deptBudgetQuery.SetField("company.departments.budget")

	deptQuery = query.NewConjunctionQuery([]query.Query{deptNameQuery, deptBudgetQuery})

	req = buildReq([]query.Query{locQuery, deptQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hits, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc1" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	// Test 5a: Find companies with an employee named "Frank" AND role "Manager" whose department is
	// handling a project titled "Project Beta" which is marked as "completed"
	empNameQuery = query.NewMatchQuery("Frank")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery := query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	projTitleQuery = query.NewMatchPhraseQuery("Project Beta")
	projTitleQuery.SetField("company.departments.projects.title")

	projStatusQuery = query.NewMatchQuery("completed")
	projStatusQuery.SetField("company.departments.projects.status")

	projQuery := query.NewConjunctionQuery([]query.Query{projTitleQuery, projStatusQuery})

	req = buildReq([]query.Query{empQuery, projQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hit, got %d", len(res.Hits))
	}

	// Test 5b: Find companies with an employee named "Frank" AND role "Manager" whose department is
	// handling a project titled "Project Beta" which is marked as "ongoing"
	empNameQuery = query.NewMatchQuery("Frank")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery = query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	projTitleQuery = query.NewMatchPhraseQuery("Project Beta")
	projTitleQuery.SetField("company.departments.projects.title")

	projStatusQuery = query.NewMatchQuery("ongoing")
	projStatusQuery.SetField("company.departments.projects.status")

	projQuery = query.NewConjunctionQuery([]query.Query{projTitleQuery, projStatusQuery})

	req = buildReq([]query.Query{empQuery, projQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc2" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	// Test 6a: Find companies with an employee named "Eve" AND role "Manager"
	// who is working in a department located in "London, UK"
	empNameQuery = query.NewMatchQuery("Eve")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery = query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	countryQuery = query.NewMatchQuery("UK")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("London")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	req = buildReq([]query.Query{empQuery, locQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hit, got %d", len(res.Hits))
	}

	// Test 6b: Find companies with an employee named "Eve" AND role "Manager"
	// who is working in a department located in "London, Canada"
	empNameQuery = query.NewMatchQuery("Eve")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery = query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	countryQuery = query.NewMatchQuery("Canada")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("London")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	req = buildReq([]query.Query{empQuery, locQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc3" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	// Test 7a: Find companies where Ivan the Manager works London, UK

	empNameQuery = query.NewMatchQuery("Ivan")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery = query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	countryQuery = query.NewMatchQuery("UK")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("London")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	req = buildReq([]query.Query{empQuery, locQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hit, got %d", len(res.Hits))
	}

	// Test 7b: Find companies where Ivan the Manager works London, Canada

	empNameQuery = query.NewMatchQuery("Ivan")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery = query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	countryQuery = query.NewMatchQuery("Canada")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("London")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	req = buildReq([]query.Query{empQuery, locQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc3" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	// Test 8: Find companies where Frank the Manager works in Engineering department located in London, UK
	empNameQuery = query.NewMatchQuery("Frank")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery = query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	deptNameQuery = query.NewMatchQuery("Engineering")
	deptNameQuery.SetField("company.departments.name")

	deptQuery = query.NewConjunctionQuery([]query.Query{empQuery, deptNameQuery})

	countryQuery = query.NewMatchQuery("UK")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("London")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	req = buildReq([]query.Query{deptQuery, locQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc2" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	// Test 9: Match_All query must return only top-level documents
	matchAllQuery := query.NewMatchAllQuery()
	req = buildReq([]query.Query{matchAllQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 3 {
		t.Fatalf("expected 3 hits, got %d", len(res.Hits))
	}

	// Test 10: DocID query must return only top-level documents
	docIDQuery := query.NewDocIDQuery([]string{"doc1", "doc2", "doc3", "doc2_$company.locations_$0", "doc3_$company.departments_$0_$company.departments.employees_$0"})
	req = buildReq([]query.Query{docIDQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 3 {
		t.Fatalf("expected 3 hits, got %d", len(res.Hits))
	}

	// Test 11: Boolean query in Filter-only mode must return correct top-level documents
	empNameQuery = query.NewMatchQuery("Frank")
	empNameQuery.SetField("company.departments.employees.name")

	boolQuery := query.NewBooleanQuery(nil, nil, nil)
	boolQuery.AddFilter(empNameQuery)

	req = buildReq([]query.Query{boolQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 2 {
		t.Fatalf("expected 2 hits, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc2" || res.Hits[1].ID != "doc3" {
		t.Fatalf("unexpected hit IDs: %v, %v", res.Hits[0].ID, res.Hits[1].ID)
	}

	// Test 12: Boolean query Must clause should work in nested context
	empNameQuery = query.NewMatchQuery("Ivan")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery = query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	countryQuery = query.NewMatchQuery("Canada")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("London")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	boolQuery = query.NewBooleanQuery([]query.Query{empQuery, locQuery}, nil, nil)
	req = buildReq([]query.Query{boolQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc3" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	// Test 13: Queries targetting _all field should:
	// - match only top-level fields when no specific field is set
	// - not match nested fields when no specific field is set
	// - work correctly when combined with nested field queries,
	//   returning only top-level documents where both conditions are met
	allRootFieldQuery := query.NewMatchQuery("TechCorp")

	req = buildReq([]query.Query{allRootFieldQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc1" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	allNestedFieldQuery := query.NewMatchQuery("Alice")
	req = buildReq([]query.Query{allNestedFieldQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hits, got %d", len(res.Hits))
	}

	allMixedQuery := buildReq([]query.Query{allRootFieldQuery, allNestedFieldQuery})
	res, err = idx.Search(allMixedQuery)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hits, got %d", len(res.Hits))
	}

	nestedFieldQuery := query.NewMatchQuery("Alice")
	nestedFieldQuery.SetField("company.departments.employees.name")

	allMixedQueryWithNested := buildReq([]query.Query{allRootFieldQuery, nestedFieldQuery})
	res, err = idx.Search(allMixedQueryWithNested)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc1" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	empNameQuery = query.NewMatchQuery("Frank")
	empNameQuery.SetField("company.departments.employees.name")

	empRoleQuery = query.NewMatchQuery("Manager")
	empRoleQuery.SetField("company.departments.employees.role")

	empQuery = query.NewConjunctionQuery([]query.Query{empNameQuery, empRoleQuery})

	deptNameQuery = query.NewMatchQuery("Engineering")
	deptNameQuery.SetField("company.departments.name")

	deptQuery = query.NewConjunctionQuery([]query.Query{empQuery, deptNameQuery})

	countryQuery = query.NewMatchQuery("UK")
	countryQuery.SetField("company.locations.country")

	cityQuery = query.NewMatchQuery("London")
	cityQuery.SetField("company.locations.city")

	locQuery = query.NewConjunctionQuery([]query.Query{countryQuery, cityQuery})

	// mixed queries with _all field and _id field should match at root level always
	companyNameAllQuery := query.NewMatchQuery("BizInc")
	matchAllQuery = query.NewMatchAllQuery()
	req = buildReq([]query.Query{deptQuery, locQuery, companyNameAllQuery, matchAllQuery})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 1 {
		t.Fatalf("expected 1 hit, got %d", len(res.Hits))
	}
	if res.Hits[0].ID != "doc2" {
		t.Fatalf("unexpected hit ID: %v", res.Hits[0].ID)
	}

	companyNameAllQueryNoMatch := query.NewMatchQuery("WebSolutions")
	req = buildReq([]query.Query{deptQuery, locQuery, companyNameAllQueryNoMatch})
	res, err = idx.Search(req)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}
	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hits, got %d", len(res.Hits))
	}
}

func TestNestedArrayConjunctionQuery(t *testing.T) {
	imap := NewIndexMapping()
	groupsMapping := mapping.NewNestedDocumentMapping()

	nameField := mapping.NewTextFieldMapping()
	groupsMapping.AddFieldMappingsAt("first_name", nameField)
	groupsMapping.AddFieldMappingsAt("last_name", nameField)

	imap.DefaultMapping.AddSubDocumentMapping("groups", groupsMapping)

	tmpIndexPath := createTmpIndexPath(t)
	defer cleanupTmpIndexPath(t, tmpIndexPath)
	idx, err := New(tmpIndexPath, imap)
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		err = idx.Close()
		if err != nil {
			t.Fatal(err)
		}
	}()

	docs := []string{
		`{
			"groups": [
				[
					{
						"first_name": "Alice",
						"last_name": "Smith"
					},
					{
						"first_name": "Bob",
						"last_name": "Johnson"
					}
				],
				[
					{
						"first_name": "Charlie",
						"last_name": "Williams"
					},
					{
						"first_name": "Diana",
						"last_name": "Brown"
					}
				]
			]
		}`,
		`{
			"groups": [
				{
					"first_name": "Alice",
					"last_name": "Smith"
				},
				{
					"first_name": "Bob",
					"last_name": "Johnson"
				},
				{
					"first_name": "Charlie",
					"last_name": "Williams"
				},
				{
					"first_name": "Diana",
					"last_name": "Brown"
				}
			]
		}`,
	}

	for i, doc := range docs {
		var dataMap map[string]interface{}
		err := json.Unmarshal([]byte(doc), &dataMap)
		if err != nil {
			t.Fatalf("failed to unmarshal document %d: %v", i, err)
		}
		err = idx.Index(fmt.Sprintf("%d", i+1), dataMap)
		if err != nil {
			t.Fatalf("failed to index document %d: %v", i, err)
		}
	}

	var (
		firstNameQuery *query.MatchQuery
		lastNameQuery  *query.MatchQuery
		conjQuery      *query.ConjunctionQuery
		searchReq      *SearchRequest
		res            *SearchResult
	)

	// Search for documents where first_name is "Alice" AND last_name is "Johnson"
	firstNameQuery = query.NewMatchQuery("Alice")
	firstNameQuery.SetField("groups.first_name")

	lastNameQuery = query.NewMatchQuery("Johnson")
	lastNameQuery.SetField("groups.last_name")

	conjQuery = query.NewConjunctionQuery([]query.Query{firstNameQuery, lastNameQuery})

	searchReq = NewSearchRequest(conjQuery)
	searchReq.SortBy([]string{"_id"})

	res, err = idx.Search(searchReq)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}

	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hits, got %d", len(res.Hits))
	}

	// Search for documents where first_name is "Bob" AND last_name is "Johnson"
	firstNameQuery = query.NewMatchQuery("Bob")
	firstNameQuery.SetField("groups.first_name")

	lastNameQuery = query.NewMatchQuery("Johnson")
	lastNameQuery.SetField("groups.last_name")

	conjQuery = query.NewConjunctionQuery([]query.Query{firstNameQuery, lastNameQuery})

	searchReq = NewSearchRequest(conjQuery)
	searchReq.SortBy([]string{"_id"})

	res, err = idx.Search(searchReq)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}

	if len(res.Hits) != 2 {
		t.Fatalf("expected 2 hits, got %d", len(res.Hits))
	}

	if res.Hits[0].ID != "1" || res.Hits[1].ID != "2" {
		t.Fatalf("unexpected hit IDs: %v, %v", res.Hits[0].ID, res.Hits[1].ID)
	}

	// Search for documents where first_name is "Alice" AND last_name is "Williams"
	firstNameQuery = query.NewMatchQuery("Alice")
	firstNameQuery.SetField("groups.first_name")

	lastNameQuery = query.NewMatchQuery("Williams")
	lastNameQuery.SetField("groups.last_name")

	conjQuery = query.NewConjunctionQuery([]query.Query{firstNameQuery, lastNameQuery})

	searchReq = NewSearchRequest(conjQuery)
	searchReq.SortBy([]string{"_id"})

	res, err = idx.Search(searchReq)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}

	if len(res.Hits) != 0 {
		t.Fatalf("expected 0 hits, got %d", len(res.Hits))
	}

	// Search for documents where first_name is "Diana" AND last_name is "Brown"
	firstNameQuery = query.NewMatchQuery("Diana")
	firstNameQuery.SetField("groups.first_name")

	lastNameQuery = query.NewMatchQuery("Brown")
	lastNameQuery.SetField("groups.last_name")

	conjQuery = query.NewConjunctionQuery([]query.Query{firstNameQuery, lastNameQuery})

	searchReq = NewSearchRequest(conjQuery)
	searchReq.SortBy([]string{"_id"})

	res, err = idx.Search(searchReq)
	if err != nil {
		t.Fatalf("search failed: %v", err)
	}

	if len(res.Hits) != 2 {
		t.Fatalf("expected 2 hits, got %d", len(res.Hits))
	}

	if res.Hits[0].ID != "1" || res.Hits[1].ID != "2" {
		t.Fatalf("unexpected hit IDs: %v, %v", res.Hits[0].ID, res.Hits[1].ID)
	}
}

func TestValidNestedMapping(t *testing.T) {
	// ensure that top-level mappings - DefaultMapping and any type mappings - cannot be nested mappings
	imap := mapping.NewIndexMapping()
	nestedMapping := mapping.NewNestedDocumentMapping()
	imap.DefaultMapping = nestedMapping
	err := imap.Validate()
	if err == nil {
		t.Fatalf("expected error for nested DefaultMapping, got nil")
	}
	// invalid nested type mapping
	imap = mapping.NewIndexMapping()
	imap.AddDocumentMapping("type1", nestedMapping)
	err = imap.Validate()
	if err == nil {
		t.Fatalf("expected error for nested type mapping, got nil")
	}
	// valid nested mappings within DefaultMapping
	imap = mapping.NewIndexMapping()
	docMapping := mapping.NewDocumentMapping()
	nestedMapping = mapping.NewNestedDocumentMapping()
	fieldMapping := mapping.NewTextFieldMapping()
	nestedMapping.AddFieldMappingsAt("field1", fieldMapping)
	docMapping.AddSubDocumentMapping("nestedField", nestedMapping)
	imap.DefaultMapping = docMapping
	err = imap.Validate()
	if err != nil {
		t.Fatalf("expected valid nested mapping, got error: %v", err)
	}
	// valid nested mappings within type mapping
	imap = mapping.NewIndexMapping()
	docMapping = mapping.NewDocumentMapping()
	nestedMapping = mapping.NewNestedDocumentMapping()
	fieldMapping = mapping.NewTextFieldMapping()
	nestedMapping.AddFieldMappingsAt("field1", fieldMapping)
	docMapping.AddSubDocumentMapping("nestedField", nestedMapping)
	imap.AddDocumentMapping("type1", docMapping)
	err = imap.Validate()
	if err != nil {
		t.Fatalf("expected valid nested mapping, got error: %v", err)
	}
	// some nested type mappings
	imap = mapping.NewIndexMapping()
	nestedMapping = mapping.NewNestedDocumentMapping()
	regularMapping := mapping.NewDocumentMapping()
	imap.AddDocumentMapping("non_nested1", regularMapping)
	imap.AddDocumentMapping("non_nested2", regularMapping)
	imap.AddDocumentMapping("nested1", nestedMapping)
	imap.AddDocumentMapping("nested2", nestedMapping)
	err = imap.Validate()
	if err == nil {
		t.Fatalf("expected error for nested type mappings, got nil")
	}
}
