package topics

import (
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/mochi-co/mqtt/server/internal/packets"
)

func TestNew(t *testing.T) {
	index := New()
	require.NotNil(t, index)
	require.NotNil(t, index.Root)
}

func BenchmarkNew(b *testing.B) {
	for n := 0; n < b.N; n++ {
		New()
	}
}

func TestPoperate(t *testing.T) {
	index := New()
	child := index.poperate("path/to/my/mqtt")
	require.Equal(t, "mqtt", child.Key)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"])

	child = index.poperate("a/b/c/d/e")
	require.Equal(t, "e", child.Key)
	child = index.poperate("a/b/c/c/a")
	require.Equal(t, "a", child.Key)
}

func BenchmarkPoperate(b *testing.B) {
	index := New()
	for n := 0; n < b.N; n++ {
		index.poperate("path/to/my/mqtt")
	}
}

func TestUnpoperate(t *testing.T) {
	index := New()
	index.Subscribe("path/to/my/mqtt", "client-1", 0)
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Clients, "client-1")

	index.Subscribe("path/to/another/mqtt", "client-1", 0)
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Clients, "client-1")

	pk := packets.Packet{TopicName: "path/to/retained/message", Payload: []byte{'h', 'e', 'l', 'l', 'o'}}
	index.RetainMessage(pk)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["retained"].Leaves["message"])
	require.Equal(t, pk, index.Root.Leaves["path"].Leaves["to"].Leaves["retained"].Leaves["message"].Message)

	pk2 := packets.Packet{TopicName: "path/to/my/mqtt", Payload: []byte{'s', 'h', 'a', 'r', 'e', 'd'}}
	index.RetainMessage(pk2)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"])
	require.Equal(t, pk2, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Message)

	index.unpoperate("path/to/my/mqtt", "", true) // delete retained
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Clients, "client-1")
	require.Equal(t, false, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Message.FixedHeader.Retain)

	index.unpoperate("path/to/my/mqtt", "client-1", false) // unsubscribe client
	require.Nil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"])

	index.unpoperate("path/to/retained/message", "", true) // delete retained
	require.NotContains(t, index.Root.Leaves["path"].Leaves["to"].Leaves, "my")

	index.unpoperate("path/to/whatever", "client-1", false) // unsubscribe client
	require.Nil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"])

	//require.Empty(t, index.Root.Leaves["path"])

}

func BenchmarkUnpoperate(b *testing.B) {
	index := New()
	for n := 0; n < b.N; n++ {
		index.poperate("path/to/my/mqtt")
	}
}

func TestRetainMessage(t *testing.T) {
	pk := packets.Packet{
		FixedHeader: packets.FixedHeader{
			Retain: true,
		},
		TopicName: "path/to/my/mqtt",
		Payload:   []byte{'h', 'e', 'l', 'l', 'o'},
	}
	pk2 := packets.Packet{
		FixedHeader: packets.FixedHeader{
			Retain: true,
		},
		TopicName: "path/to/another/mqtt",
		Payload:   []byte{'h', 'e', 'l', 'l', 'o'},
	}

	index := New()
	q := index.RetainMessage(pk)
	require.Equal(t, int64(1), q)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"])
	require.Equal(t, pk, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Message)

	index.Subscribe("path/to/another/mqtt", "client-1", 0)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Clients["client-1"])
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"])

	q = index.RetainMessage(pk2)
	require.Equal(t, int64(1), q)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"])
	require.Equal(t, pk2, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Message)
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Clients, "client-1")

	// The same message already exists, but we're not doing a deep-copy check, so it's considered
	// to be a new message.
	q = index.RetainMessage(pk2)
	require.Equal(t, int64(1), q)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"])
	require.Equal(t, pk2, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Message)
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Clients, "client-1")

	// Delete retained
	pk3 := packets.Packet{TopicName: "path/to/another/mqtt", Payload: []byte{}}
	q = index.RetainMessage(pk3)
	require.Equal(t, int64(-1), q)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"])
	require.Equal(t, pk, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Message)
	require.Equal(t, false, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Message.FixedHeader.Retain)

	// Second Delete retained
	q = index.RetainMessage(pk3)
	require.Equal(t, int64(0), q)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"])
	require.Equal(t, pk, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Message)
	require.Equal(t, false, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Message.FixedHeader.Retain)

}

func BenchmarkRetainMessage(b *testing.B) {
	index := New()
	pk := packets.Packet{TopicName: "path/to/another/mqtt"}
	for n := 0; n < b.N; n++ {
		index.RetainMessage(pk)
	}
}

func TestSubscribeOK(t *testing.T) {
	index := New()

	q := index.Subscribe("path/to/my/mqtt", "client-1", 0)
	require.Equal(t, true, q)

	q = index.Subscribe("path/to/my/mqtt", "client-1", 0)
	require.Equal(t, false, q)

	q = index.Subscribe("path/to/my/mqtt", "client-2", 0)
	require.Equal(t, true, q)

	q = index.Subscribe("path/to/another/mqtt", "client-1", 0)
	require.Equal(t, true, q)

	q = index.Subscribe("path/+", "client-2", 0)
	require.Equal(t, true, q)

	q = index.Subscribe("#", "client-3", 0)
	require.Equal(t, true, q)

	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Clients, "client-1")
	require.Equal(t, "path/to/my/mqtt", index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Filter)
	require.Equal(t, "mqtt", index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Key)
	require.Equal(t, index.Root.Leaves["path"], index.Root.Leaves["path"].Leaves["to"].Parent)
	require.NotNil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Clients, "client-2")

	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["another"].Leaves["mqtt"].Clients, "client-1")
	require.Contains(t, index.Root.Leaves["path"].Leaves["+"].Clients, "client-2")
	require.Contains(t, index.Root.Leaves["#"].Clients, "client-3")
}

func BenchmarkSubscribe(b *testing.B) {
	index := New()
	for n := 0; n < b.N; n++ {
		index.Subscribe("path/to/mqtt/basic", "client-1", 0)
	}
}

func TestUnsubscribeA(t *testing.T) {
	index := New()
	index.Subscribe("path/to/my/mqtt", "client-1", 0)
	index.Subscribe("path/to/+/mqtt", "client-1", 0)
	index.Subscribe("path/to/stuff", "client-1", 0)
	index.Subscribe("path/to/stuff", "client-2", 0)
	index.Subscribe("#", "client-3", 0)
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"].Leaves["mqtt"].Clients, "client-1")
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["+"].Leaves["mqtt"].Clients, "client-1")
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["stuff"].Clients, "client-1")
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["stuff"].Clients, "client-2")
	require.Contains(t, index.Root.Leaves["#"].Clients, "client-3")

	ok := index.Unsubscribe("path/to/my/mqtt", "client-1")
	require.Equal(t, true, ok)

	require.Nil(t, index.Root.Leaves["path"].Leaves["to"].Leaves["my"])
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["+"].Leaves["mqtt"].Clients, "client-1")

	ok = index.Unsubscribe("path/to/stuff", "client-1")
	require.Equal(t, true, ok)

	require.NotContains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["stuff"].Clients, "client-1")
	require.Contains(t, index.Root.Leaves["path"].Leaves["to"].Leaves["stuff"].Clients, "client-2")
	require.Contains(t, index.Root.Leaves["#"].Clients, "client-3")

	ok = index.Unsubscribe("fdasfdas/dfsfads/sa", "client-1")
	require.Equal(t, false, ok)

}

func TestUnsubscribeCascade(t *testing.T) {
	index := New()
	index.Subscribe("a/b/c", "client-1", 0)
	index.Subscribe("a/b/c/e/e", "client-1", 0)

	ok := index.Unsubscribe("a/b/c/e/e", "client-1")
	require.Equal(t, true, ok)
	require.NotEmpty(t, index.Root.Leaves)
	require.Contains(t, index.Root.Leaves["a"].Leaves["b"].Leaves["c"].Clients, "client-1")
}

// This benchmark is Unsubscribe-Subscribe
func BenchmarkUnsubscribe(b *testing.B) {
	index := New()

	for n := 0; n < b.N; n++ {
		index.Subscribe("path/to/my/mqtt", "client-1", 0)
		index.Unsubscribe("path/to/mqtt/basic", "client-1")
	}
}

func TestSubscribersFind(t *testing.T) {
	tt := []struct {
		filter string
		topic  string
		len    int
	}{
		{
			filter: "a",
			topic:  "a",
			len:    1,
		},
		{
			filter: "a/",
			topic:  "a",
			len:    0,
		},
		{
			filter: "a/",
			topic:  "a/",
			len:    1,
		},
		{
			filter: "/a",
			topic:  "/a",
			len:    1,
		},
		{
			filter: "path/to/my/mqtt",
			topic:  "path/to/my/mqtt",
			len:    1,
		},
		{
			filter: "path/to/+/mqtt",
			topic:  "path/to/my/mqtt",
			len:    1,
		},
		{
			filter: "+/to/+/mqtt",
			topic:  "path/to/my/mqtt",
			len:    1,
		},
		{
			filter: "#",
			topic:  "path/to/my/mqtt",
			len:    1,
		},
		{
			filter: "+/+/+/+",
			topic:  "path/to/my/mqtt",
			len:    1,
		},
		{
			filter: "+/+/+/#",
			topic:  "path/to/my/mqtt",
			len:    1,
		},
		{
			filter: "zen/#",
			topic:  "zen",
			len:    1,
		},
		{
			filter: "+/+/#",
			topic:  "path/to/my/mqtt",
			len:    1,
		},
		{
			filter: "path/to/",
			topic:  "path/to/my/mqtt",
			len:    0,
		},
		{
			filter: "#/stuff",
			topic:  "path/to/my/mqtt",
			len:    0,
		},
		{
			filter: "$SYS/#",
			topic:  "$SYS/info",
			len:    1,
		},
		{
			filter: "#",
			topic:  "$SYS/info",
			len:    0,
		},
		{
			filter: "+/info",
			topic:  "$SYS/info",
			len:    0,
		},
	}

	for i, check := range tt {
		index := New()
		index.Subscribe(check.filter, "client-1", 0)
		clients := index.Subscribers(check.topic)
		//spew.Dump(clients)
		require.Equal(t, check.len, len(clients), "Unexpected clients len at %d %s %s", i, check.filter, check.topic)
	}

}

func BenchmarkSubscribers(b *testing.B) {
	index := New()
	index.Subscribe("path/to/my/mqtt", "client-1", 0)
	index.Subscribe("path/to/+/mqtt", "client-1", 0)
	index.Subscribe("something/things/stuff/+", "client-1", 0)
	index.Subscribe("path/to/stuff", "client-2", 0)
	index.Subscribe("#", "client-3", 0)

	for n := 0; n < b.N; n++ {
		index.Subscribers("path/to/testing/mqtt")
	}
}

func TestIsolateParticle(t *testing.T) {
	particle, hasNext := isolateParticle("path/to/my/mqtt", 0)
	require.Equal(t, "path", particle)
	require.Equal(t, true, hasNext)
	particle, hasNext = isolateParticle("path/to/my/mqtt", 1)
	require.Equal(t, "to", particle)
	require.Equal(t, true, hasNext)
	particle, hasNext = isolateParticle("path/to/my/mqtt", 2)
	require.Equal(t, "my", particle)
	require.Equal(t, true, hasNext)
	particle, hasNext = isolateParticle("path/to/my/mqtt", 3)
	require.Equal(t, "mqtt", particle)
	require.Equal(t, false, hasNext)

	particle, hasNext = isolateParticle("/path/", 0)
	require.Equal(t, "", particle)
	require.Equal(t, true, hasNext)
	particle, hasNext = isolateParticle("/path/", 1)
	require.Equal(t, "path", particle)
	require.Equal(t, true, hasNext)
	particle, hasNext = isolateParticle("/path/", 2)
	require.Equal(t, "", particle)
	require.Equal(t, false, hasNext)

	particle, hasNext = isolateParticle("a/b/c/+/+", 3)
	require.Equal(t, "+", particle)
	require.Equal(t, true, hasNext)
	particle, hasNext = isolateParticle("a/b/c/+/+", 4)
	require.Equal(t, "+", particle)
	require.Equal(t, false, hasNext)
}

func BenchmarkIsolateParticle(b *testing.B) {
	for n := 0; n < b.N; n++ {
		isolateParticle("path/to/my/mqtt", 3)
	}
}

func TestMessagesPattern(t *testing.T) {
	tt := []struct {
		packet packets.Packet
		filter string
		len    int
	}{
		{
			packets.Packet{TopicName: "a/b/c/d", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"a/b/c/d",
			1,
		},
		{
			packets.Packet{TopicName: "a/b/c/e", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"a/+/c/+",
			2,
		},
		{
			packets.Packet{TopicName: "a/b/d/f", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"+/+/+/+",
			3,
		},
		{
			packets.Packet{TopicName: "q/w/e/r/t/y", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"q/w/e/#",
			1,
		},
		{
			packets.Packet{TopicName: "q/w/x/r/t/x", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"q/#",
			2,
		},
		{
			packets.Packet{TopicName: "asd", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"asd",
			1,
		},
		{
			packets.Packet{TopicName: "$SYS/testing", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"#",
			8,
		},
		{
			packets.Packet{TopicName: "$SYS/test", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"+/testing",
			0,
		},
		{
			packets.Packet{TopicName: "$SYS/info", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"$SYS/info",
			1,
		},
		{
			packets.Packet{TopicName: "$SYS/b", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"$SYS/#",
			4,
		},
		{
			packets.Packet{TopicName: "asd/fgh/jkl", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"#",
			8,
		},
		{
			packets.Packet{TopicName: "stuff/asdadsa/dsfdsafdsadfsa/dsfdsf/sdsadas", Payload: []byte{'h', 'e', 'l', 'l', 'o'}, FixedHeader: packets.FixedHeader{Retain: true}},
			"stuff/#/things", // indexer will ignore trailing /things
			1,
		},
	}
	index := New()
	for _, check := range tt {
		index.RetainMessage(check.packet)
	}

	for i, check := range tt {
		messages := index.Messages(check.filter)
		require.Equal(t, check.len, len(messages), "Unexpected messages len at %d %s %s", i, check.filter, check.packet.TopicName)
	}
}

func TestMessagesFind(t *testing.T) {
	index := New()
	index.RetainMessage(packets.Packet{TopicName: "a/a", Payload: []byte{'a'}, FixedHeader: packets.FixedHeader{Retain: true}})
	index.RetainMessage(packets.Packet{TopicName: "a/b", Payload: []byte{'b'}, FixedHeader: packets.FixedHeader{Retain: true}})
	messages := index.Messages("a/a")
	require.Equal(t, 1, len(messages))

	messages = index.Messages("a/+")
	require.Equal(t, 2, len(messages))
}

func BenchmarkMessages(b *testing.B) {
	index := New()
	index.RetainMessage(packets.Packet{TopicName: "path/to/my/mqtt"})
	index.RetainMessage(packets.Packet{TopicName: "path/to/another/mqtt"})
	index.RetainMessage(packets.Packet{TopicName: "path/a/some/mqtt"})
	index.RetainMessage(packets.Packet{TopicName: "what/is"})
	index.RetainMessage(packets.Packet{TopicName: "q/w/e/r/t/y"})

	for n := 0; n < b.N; n++ {
		index.Messages("path/to/+/mqtt")
	}
}
