package issues_indexer

import (
	"context"
	"os"
	"strings"
	"testing"

	"github.com/blevesearch/bleve/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/pagination"
	"tangled.org/core/appview/searchquery"
)

func setupTestIndexer(t *testing.T) (*Indexer, func()) {
	t.Helper()

	tmpDir, err := os.MkdirTemp("", "issue_indexer_test")
	require.NoError(t, err)

	ix := NewIndexer(tmpDir)

	mapping, err := generateIssueIndexMapping()
	require.NoError(t, err)

	indexer, err := bleve.New(tmpDir, mapping)
	require.NoError(t, err)
	ix.indexer = indexer

	cleanup := func() {
		ix.indexer.Close()
		os.RemoveAll(tmpDir)
	}

	return ix, cleanup
}

func boolPtr(b bool) *bool { return &b }

// makeLabelState creates a LabelState for testing. Each entry is either
// "name" (null-type label) or "name=val" (valued label).
func makeLabelState(entries ...string) models.LabelState {
	state := models.NewLabelState()
	for _, entry := range entries {
		if eqIdx := strings.Index(entry, "="); eqIdx > 0 {
			name := entry[:eqIdx]
			val := entry[eqIdx+1:]
			if state.Inner()[name] == nil {
				state.Inner()[name] = make(map[string]struct{})
			}
			state.Inner()[name][val] = struct{}{}
			state.SetName(name, name)
		} else {
			state.Inner()[entry] = make(map[string]struct{})
			state.SetName(entry, entry)
		}
	}
	return state
}

func TestSearchFilters(t *testing.T) {
	ix, cleanup := setupTestIndexer(t)
	defer cleanup()

	ctx := context.Background()

	err := ix.Index(ctx,
		models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")},
		models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob", Labels: makeLabelState("feature")},
		models.Issue{Id: 3, RepoDid: "did:plc:testrepo", Title: "Fix login timeout", Body: "Login takes too long", Open: false, Did: "did:plc:alice", Labels: makeLabelState("bug")},
	)
	require.NoError(t, err)

	opts := func() models.IssueSearchOptions {
		return models.IssueSearchOptions{
			RepoDid: "did:plc:testrepo",
			IsOpen:  boolPtr(true),
			Page:    pagination.Page{Limit: 10},
		}
	}

	// Keyword in title
	o := opts()
	o.Keywords = []string{"bug"}
	result, err := ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(1))

	// Keyword in body
	o = opts()
	o.Keywords = []string{"theme"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(2))

	// Phrase match
	o = opts()
	o.Phrases = []string{"login bug"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(1))

	// Author filter
	o = opts()
	o.AuthorDid = "did:plc:alice"
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(1))

	// Label filter
	o = opts()
	o.Labels = []string{"bug"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(1))

	// State filter (closed)
	o = opts()
	o.IsOpen = boolPtr(false)
	o.Labels = []string{"bug"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(3))

	// Combined: keyword + author + label
	o = opts()
	o.Keywords = []string{"login"}
	o.AuthorDid = "did:plc:alice"
	o.Labels = []string{"bug"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(1))
}

func TestSearchLabelAND(t *testing.T) {
	ix, cleanup := setupTestIndexer(t)
	defer cleanup()

	ctx := context.Background()

	err := ix.Index(ctx,
		models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Issue 1", Body: "Body", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")},
		models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Issue 2", Body: "Body", Open: true, Did: "did:plc:bob", Labels: makeLabelState("bug", "urgent")},
	)
	require.NoError(t, err)

	result, err := ix.Search(ctx, models.IssueSearchOptions{
		RepoDid: "did:plc:testrepo",
		IsOpen:  boolPtr(true),
		Labels:  []string{"bug", "urgent"},
		Page:    pagination.Page{Limit: 10},
	})
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(2))
}

func TestSearchNegation(t *testing.T) {
	ix, cleanup := setupTestIndexer(t)
	defer cleanup()

	ctx := context.Background()

	err := ix.Index(ctx,
		models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")},
		models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob", Labels: makeLabelState("feature")},
		models.Issue{Id: 3, RepoDid: "did:plc:testrepo", Title: "Fix timeout bug", Body: "Timeout on save", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug", "urgent")},
	)
	require.NoError(t, err)

	opts := func() models.IssueSearchOptions {
		return models.IssueSearchOptions{
			RepoDid: "did:plc:testrepo",
			IsOpen:  boolPtr(true),
			Page:    pagination.Page{Limit: 10},
		}
	}

	// Negated label: exclude "bug", should only return issue 2
	o := opts()
	o.NegatedLabels = []string{"bug"}
	result, err := ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(2))

	// Negated keyword: exclude "login", should return issues 2 and 3
	o = opts()
	o.NegatedKeywords = []string{"login"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(2), result.Total)
	assert.Contains(t, result.Hits, int64(2))
	assert.Contains(t, result.Hits, int64(3))

	// Positive label + negated label: bug but not urgent
	o = opts()
	o.Labels = []string{"bug"}
	o.NegatedLabels = []string{"urgent"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(1))

	// Negated phrase
	o = opts()
	o.NegatedPhrases = []string{"dark theme"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(2), result.Total)
	assert.NotContains(t, result.Hits, int64(2))
}

func TestSearchNegatedPhraseParsed(t *testing.T) {
	ix, cleanup := setupTestIndexer(t)
	defer cleanup()

	ctx := context.Background()

	err := ix.Index(ctx,
		models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice"},
		models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob"},
		models.Issue{Id: 3, RepoDid: "did:plc:testrepo", Title: "Fix timeout bug", Body: "Timeout on save", Open: true, Did: "did:plc:alice"},
	)
	require.NoError(t, err)

	// Parse a query with a negated quoted phrase, as the handler would
	query := searchquery.Parse(`-"dark theme"`)
	var negatedPhrases []string
	for _, item := range query.Items() {
		if item.Kind == searchquery.KindQuoted && item.Negated {
			negatedPhrases = append(negatedPhrases, item.Value)
		}
	}
	require.Equal(t, []string{"dark theme"}, negatedPhrases)

	result, err := ix.Search(ctx, models.IssueSearchOptions{
		RepoDid:        "did:plc:testrepo",
		IsOpen:         boolPtr(true),
		NegatedPhrases: negatedPhrases,
		Page:           pagination.Page{Limit: 10},
	})
	require.NoError(t, err)
	assert.Equal(t, uint64(2), result.Total)
	assert.NotContains(t, result.Hits, int64(2))
}

func TestSearchNoResults(t *testing.T) {
	ix, cleanup := setupTestIndexer(t)
	defer cleanup()

	ctx := context.Background()

	err := ix.Index(ctx,
		models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "Issue", Body: "Body", Open: true, Did: "did:plc:alice"},
	)
	require.NoError(t, err)

	result, err := ix.Search(ctx, models.IssueSearchOptions{
		Keywords: []string{"nonexistent"},
		RepoDid:  "did:plc:testrepo",
		IsOpen:   boolPtr(true),
		Page:     pagination.Page{Limit: 10},
	})
	require.NoError(t, err)
	assert.Equal(t, uint64(0), result.Total)
	assert.Empty(t, result.Hits)
}

func TestSearchLabelValues(t *testing.T) {
	ix, cleanup := setupTestIndexer(t)
	defer cleanup()

	ctx := context.Background()

	err := ix.Index(ctx,
		models.Issue{Id: 1, RepoDid: "did:plc:testrepo", Title: "High priority bug", Body: "Urgent", Open: true, Did: "did:plc:alice",
			Labels: makeLabelState("bug", "priority=high")},
		models.Issue{Id: 2, RepoDid: "did:plc:testrepo", Title: "Low priority feature", Body: "Nice to have", Open: true, Did: "did:plc:bob",
			Labels: makeLabelState("feature", "priority=low")},
		models.Issue{Id: 3, RepoDid: "did:plc:testrepo", Title: "High priority feature", Body: "Important", Open: true, Did: "did:plc:alice",
			Labels: makeLabelState("feature", "priority=high")},
	)
	require.NoError(t, err)

	opts := func() models.IssueSearchOptions {
		return models.IssueSearchOptions{
			RepoDid: "did:plc:testrepo",
			IsOpen:  boolPtr(true),
			Page:    pagination.Page{Limit: 10},
		}
	}

	o := opts()
	o.LabelValues = []string{"priority:high"}
	result, err := ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(2), result.Total)
	assert.Contains(t, result.Hits, int64(1))
	assert.Contains(t, result.Hits, int64(3))

	o = opts()
	o.LabelValues = []string{"priority:low"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(2))

	// Combined: plain label + label value
	o = opts()
	o.Labels = []string{"feature"}
	o.LabelValues = []string{"priority:high"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(3))

	// Negated label value
	o = opts()
	o.NegatedLabelValues = []string{"priority:low"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(2), result.Total)
	assert.Contains(t, result.Hits, int64(1))
	assert.Contains(t, result.Hits, int64(3))

	// Label value + negated plain label
	o = opts()
	o.LabelValues = []string{"priority:high"}
	o.NegatedLabels = []string{"feature"}
	result, err = ix.Search(ctx, o)
	require.NoError(t, err)
	assert.Equal(t, uint64(1), result.Total)
	assert.Contains(t, result.Hits, int64(1))
}
