package searchquery

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestParseMixed(t *testing.T) {
	q := Parse(`state:open bug "critical issue" label:good-first-issue fix`)
	items := q.Items()
	assert.Equal(t, 5, len(items))

	assert.Equal(t, KindTagValue, items[0].Kind)
	assert.Equal(t, "state", items[0].Key)
	assert.Equal(t, "open", items[0].Value)

	assert.Equal(t, KindKeyword, items[1].Kind)
	assert.Equal(t, "bug", items[1].Raw)

	assert.Equal(t, KindQuoted, items[2].Kind)
	assert.Equal(t, `"critical issue"`, items[2].Raw)

	assert.Equal(t, KindTagValue, items[3].Kind)
	assert.Equal(t, "label", items[3].Key)
	assert.Equal(t, "good-first-issue", items[3].Value)

	assert.Equal(t, KindKeyword, items[4].Kind)

	assert.Equal(t, `state:open bug "critical issue" label:good-first-issue fix`, q.String())
}

func TestGetSetLifecycle(t *testing.T) {
	q := Parse("label:bug state:open keyword label:feature label:urgent")

	// Get returns first match
	val := q.Get("state")
	assert.NotNil(t, val)
	assert.Equal(t, "open", *val)

	// Get returns nil for missing key
	assert.Nil(t, q.Get("author"))

	// Has
	assert.True(t, q.Has("state"))
	assert.False(t, q.Has("author"))

	// GetAll
	assert.Equal(t, []string{"bug", "feature", "urgent"}, q.GetAll("label"))
	assert.Equal(t, 0, len(q.GetAll("missing")))

	// Set updates existing, preserving position
	q.Set("state", "closed")
	assert.Equal(t, "label:bug state:closed keyword label:feature label:urgent", q.String())

	// Set deduplicates
	q.Set("label", "single")
	assert.Equal(t, "label:single state:closed keyword", q.String())

	// Set appends new tag
	q.Set("author", "bob")
	assert.Equal(t, "label:single state:closed keyword author:bob", q.String())
}

func TestParseEmpty(t *testing.T) {
	q := Parse("   ")
	assert.Equal(t, 0, len(q.Items()))
	assert.Equal(t, "", q.String())
}

func TestParseUnclosedQuote(t *testing.T) {
	q := Parse(`"hello world`)
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindQuoted, items[0].Kind)
	assert.Equal(t, `"hello world`, items[0].Raw)
	assert.Equal(t, "hello world", items[0].Value)
}

func TestParseLeadingColon(t *testing.T) {
	q := Parse(":value")
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindKeyword, items[0].Kind)
	assert.Equal(t, ":value", items[0].Raw)
}

func TestParseColonInValue(t *testing.T) {
	q := Parse("key:value:with:colons")
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, "key", items[0].Key)
	assert.Equal(t, "value:with:colons", items[0].Value)
}

func TestParseEmptyValue(t *testing.T) {
	q := Parse("state:")
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindTagValue, items[0].Kind)
	assert.Equal(t, "state", items[0].Key)
	assert.Equal(t, "", items[0].Value)
}

func TestQuotedKeyValueIsNotTag(t *testing.T) {
	q := Parse(`"state:open"`)
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindQuoted, items[0].Kind)
	assert.Equal(t, "state:open", items[0].Value)
	assert.False(t, q.Has("state"))
}

func TestConsecutiveQuotes(t *testing.T) {
	q := Parse(`"one""two"`)
	items := q.Items()
	assert.Equal(t, 2, len(items))
	assert.Equal(t, `"one"`, items[0].Raw)
	assert.Equal(t, `"two"`, items[1].Raw)
}

func TestEscapedQuotes(t *testing.T) {
	q := Parse(`"hello \"world\""`)
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindQuoted, items[0].Kind)
	assert.Equal(t, `"hello \"world\""`, items[0].Raw)
	assert.Equal(t, `hello "world"`, items[0].Value)
}

func TestEscapedBackslash(t *testing.T) {
	q := Parse(`"hello\\"`)
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindQuoted, items[0].Kind)
	assert.Equal(t, `hello\`, items[0].Value)
}

func TestNegatedTag(t *testing.T) {
	q := Parse("state:open -label:bug keyword -label:wontfix")
	items := q.Items()
	assert.Equal(t, 4, len(items))

	assert.False(t, items[0].Negated)
	assert.Equal(t, "state", items[0].Key)

	assert.True(t, items[1].Negated)
	assert.Equal(t, KindTagValue, items[1].Kind)
	assert.Equal(t, "label", items[1].Key)
	assert.Equal(t, "bug", items[1].Value)
	assert.Equal(t, "-label:bug", items[1].Raw)

	assert.True(t, items[3].Negated)
	assert.Equal(t, "wontfix", items[3].Value)

	// Get/GetAll/Has skip negated tags
	assert.False(t, q.Has("label"))
	assert.Equal(t, 0, len(q.GetAll("label")))

	// Set doesn't touch negated tags
	q.Set("label", "feature")
	assert.Equal(t, "state:open -label:bug keyword -label:wontfix label:feature", q.String())
}

func TestNegatedBareWordIsKeyword(t *testing.T) {
	q := Parse("-keyword")
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindKeyword, items[0].Kind)
	assert.Equal(t, "-keyword", items[0].Raw)
}

func TestNegatedQuotedPhrase(t *testing.T) {
	q := Parse(`-"critical bug" state:open`)
	items := q.Items()
	assert.Equal(t, 2, len(items))

	assert.Equal(t, KindQuoted, items[0].Kind)
	assert.True(t, items[0].Negated)
	assert.Equal(t, `-"critical bug"`, items[0].Raw)
	assert.Equal(t, "critical bug", items[0].Value)

	assert.Equal(t, KindTagValue, items[1].Kind)
	assert.Equal(t, "state", items[1].Key)
}

func TestNegatedQuotedPhraseAmongOthers(t *testing.T) {
	q := Parse(`"good phrase" -"bad phrase" keyword`)
	items := q.Items()
	assert.Equal(t, 3, len(items))

	assert.Equal(t, KindQuoted, items[0].Kind)
	assert.False(t, items[0].Negated)
	assert.Equal(t, "good phrase", items[0].Value)

	assert.Equal(t, KindQuoted, items[1].Kind)
	assert.True(t, items[1].Negated)
	assert.Equal(t, "bad phrase", items[1].Value)

	assert.Equal(t, KindKeyword, items[2].Kind)
}

func TestWhitespaceNormalization(t *testing.T) {
	q := Parse("   state:open   keyword   ")
	assert.Equal(t, "state:open keyword", q.String())
}

func TestParseConsecutiveColons(t *testing.T) {
	q := Parse("foo:::bar")
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindTagValue, items[0].Kind)
	assert.Equal(t, "foo", items[0].Key)
	assert.Equal(t, "::bar", items[0].Value)
}

func TestParseEmptyQuotes(t *testing.T) {
	q := Parse(`""`)
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindQuoted, items[0].Kind)
	assert.Equal(t, `""`, items[0].Raw)
	assert.Equal(t, "", items[0].Value)
}

func TestParseBareDash(t *testing.T) {
	q := Parse("-")
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindKeyword, items[0].Kind)
	assert.False(t, items[0].Negated)
	assert.Equal(t, "-", items[0].Raw)
}

func TestParseDashColon(t *testing.T) {
	q := Parse("-:value")
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindKeyword, items[0].Kind)
	assert.True(t, items[0].Negated)
	assert.Equal(t, ":value", items[0].Value)
}

func TestParseDoubleHyphen(t *testing.T) {
	q := Parse("--label:bug")
	items := q.Items()
	assert.Equal(t, 1, len(items))
	assert.Equal(t, KindTagValue, items[0].Kind)
	assert.True(t, items[0].Negated)
	assert.Equal(t, "-label", items[0].Key)
	assert.Equal(t, "bug", items[0].Value)
}

func TestDynamicTags(t *testing.T) {
	q := Parse("state:open label:bug priority:high severity:critical -priority:low author:alice -severity:minor keyword")

	// Known tags are not included
	dynamic := q.GetDynamicTags()
	assert.Equal(t, []string{"priority:high", "severity:critical"}, dynamic)

	negated := q.GetNegatedDynamicTags()
	assert.Equal(t, []string{"priority:low", "severity:minor"}, negated)

	// Known tags still work as before
	assert.Equal(t, []string{"bug"}, q.GetAll("label"))
	val := q.Get("state")
	assert.NotNil(t, val)
	assert.Equal(t, "open", *val)
}

func TestDynamicTagsEmpty(t *testing.T) {
	q := Parse("state:open label:bug author:alice keyword")
	assert.Equal(t, 0, len(q.GetDynamicTags()))
	assert.Equal(t, 0, len(q.GetNegatedDynamicTags()))
}
