package watch_graph //nolint:staticcheck

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"go.uber.org/mock/gomock"
)

type (
	vid int8
	at  int16
	vd  int32
	ad  int64
)

const (
	vid1 vid = 1
	vid2 vid = 2
	vid3 vid = 21
	vid4 vid = 22

	vd1 vd = 3
	vd2 vd = 4

	ad1 ad = 5
	ad2 ad = 6

	at1 at = 7
)

func TestObjectGraph_SetVertex_NoArcs(t *testing.T) {
	g, o := setup(t)
	gomock.InOrder(
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd2),
	)

	res := g.SetVertex(t.Context(), vid1, vd1, nil)
	assert.Equal(t, VertexAdded, res)
	assert.Empty(t, g.InboundArcsFor(vid1))

	res = g.SetVertex(t.Context(), vid1, vd1, nil)
	assert.Equal(t, VertexNoop, res)
	assert.Empty(t, g.InboundArcsFor(vid1))

	res = g.SetVertex(t.Context(), vid1, vd2, nil)
	assert.Equal(t, VertexUpdated, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
}

func TestObjectGraph_SetVertex_WithArcs(t *testing.T) {
	g, o := setup(t)
	arcTo2 := arc2id(vid2, at1)
	gomock.InOrder(
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad1),
		o.EXPECT().OnDeleteArc(gomock.Any(), gomock.Any(), vid1, arcTo2),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad1),
	)

	// Inline ArcSetWithData because SetVertex() takes ownership of it and hence we must not reuse the object.

	res := g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad1}) // new vertex + arcs
	assert.Equal(t, VertexAdded, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid2))

	res = g.SetVertex(t.Context(), vid1, vd1, nil) // remove arcs
	assert.Equal(t, VertexNoop, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Empty(t, g.InboundArcsFor(vid2))

	res = g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad1}) // old vertex, new arcs
	assert.Equal(t, VertexNoop, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid2))
}

func TestObjectGraph_SetVertex_WithArcs_ReplaceDataOnly(t *testing.T) {
	g, o := setup(t)
	arcTo2 := arc2id(vid2, at1)
	gomock.InOrder(
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad1),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad2),
	)

	// Inline ArcSetWithData because SetVertex() takes ownership of it and hence we must not reuse the object.

	res := g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad1}) // new vertex + arcs
	assert.Equal(t, VertexAdded, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid2))

	res = g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad2}) // old vertex, old arc with new data
	assert.Equal(t, VertexNoop, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid2))
}

func TestObjectGraph_SetVertex_WithArcs_ReplaceAll(t *testing.T) {
	g, o := setup(t)
	arcTo2 := arc2id(vid2, at1)
	arcTo3 := arc2id(vid3, at1)
	gomock.InOrder(
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad1),
		o.EXPECT().OnDeleteArc(gomock.Any(), gomock.Any(), vid1, arcTo2),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo3, ad2),
	)

	// Inline ArcSetWithData because SetVertex() takes ownership of it and hence we must not reuse the object.

	res := g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad1}) // new vertex + arcs
	assert.Equal(t, VertexAdded, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid2))

	res = g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo3: ad2}) // old vertex, new arc instead of old arc
	assert.Equal(t, VertexNoop, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Empty(t, g.InboundArcsFor(vid2))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid3))
}

func TestObjectGraph_SetVertex_WithArcs_ReplaceSome(t *testing.T) {
	g, o := setup(t)
	arcTo2 := arc2id(vid2, at1)
	arcTo3 := arc2id(vid3, at1)
	arcTo4 := arc2id(vid4, at1)

	// Iteration order is not deterministic so define partial orderings
	sv := o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1)
	sa2 := o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad1)
	sa3 := o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo3, ad2)

	// set arcs after set vertex
	gomock.InOrder(sv, sa2)
	gomock.InOrder(sv, sa3)

	// delete arc after set arcs
	da := o.EXPECT().OnDeleteArc(gomock.Any(), gomock.Any(), vid1, arcTo2)
	gomock.InOrder(sa2, da)
	gomock.InOrder(sa3, da)

	// set arc after delete arc
	gomock.InOrder(
		da,
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo4, ad2),
	)

	// Inline ArcSetWithData because SetVertex() takes ownership of it and hence we must not reuse the object.

	res := g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad1, arcTo3: ad2}) // new vertex + arcs
	assert.Equal(t, VertexAdded, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid2))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid3))

	res = g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo4: ad2, arcTo3: ad2}) // old vertex, new arc instead of old arc
	assert.Equal(t, VertexNoop, res)
	assert.Empty(t, g.InboundArcsFor(vid1))
	assert.Empty(t, g.InboundArcsFor(vid2))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid3))
	assert.Equal(t, newArcSet(arc2id(vid1, at1)), g.InboundArcsFor(vid4))
}

func TestObjectGraph_SetVertex_NoArcsThenSetArcs(t *testing.T) {
	g, o := setup(t)
	arcTo2 := arc2id(vid2, at1)
	gomock.InOrder(
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad1),
	)

	// Inline ArcSetWithData because SetVertex() takes ownership of it and hence we must not reuse the object.

	res := g.SetVertex(t.Context(), vid1, vd1, nil) // new vertex, no arcs
	assert.Equal(t, VertexAdded, res)

	res = g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad1}) // old vertex, set arcs
	assert.Equal(t, VertexNoop, res)
}

func TestObjectGraph_DeleteVertex_WithArcs(t *testing.T) {
	g, o := setup(t)
	arcTo2 := arc2id(vid2, at1)
	gomock.InOrder(
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad1),
		o.EXPECT().OnDeleteArc(gomock.Any(), gomock.Any(), vid1, arcTo2),
		o.EXPECT().OnDeleteVertex(gomock.Any(), gomock.Any(), vid1),
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
		o.EXPECT().OnSetArc(gomock.Any(), gomock.Any(), vid1, arcTo2, ad1),
	)

	// Inline ArcSetWithData because SetVertex() takes ownership of it and hence we must not reuse the object.

	res := g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad1})
	assert.Equal(t, VertexAdded, res)

	assert.True(t, g.DeleteVertex(t.Context(), vid1))
	assert.False(t, g.DeleteVertex(t.Context(), vid1)) // was already removed, but check that this e.g. does not panic or call the observer again.

	res = g.SetVertex(t.Context(), vid1, vd1, ArcSetWithData[vid, at, ad]{arcTo2: ad1})
	assert.Equal(t, VertexAdded, res)
}

func TestObjectGraph_DeleteVertex_NoArcs(t *testing.T) {
	g, o := setup(t)
	gomock.InOrder(
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
		o.EXPECT().OnDeleteVertex(gomock.Any(), gomock.Any(), vid1),
		o.EXPECT().OnSetVertex(gomock.Any(), gomock.Any(), vid1, vd1),
	)

	res := g.SetVertex(t.Context(), vid1, vd1, nil)
	assert.Equal(t, VertexAdded, res)

	assert.True(t, g.DeleteVertex(t.Context(), vid1))
	assert.False(t, g.DeleteVertex(t.Context(), vid1)) // was already removed, but check that this e.g. does not panic or call the observer again.

	res = g.SetVertex(t.Context(), vid1, vd1, nil)
	assert.Equal(t, VertexAdded, res)
}

func TestObjectGraph_DeleteVertex_NoVertex(t *testing.T) {
	g, _ := setup(t)

	assert.False(t, g.DeleteVertex(t.Context(), vid1))
}

func setup(t *testing.T) (*ObjectGraph[vid, at, vd, ad], *MockObjectGraphObserver[vid, at, vd, ad]) {
	ctrl := gomock.NewController(t)
	o := NewMockObjectGraphObserver[vid, at, vd, ad](ctrl)

	g := NewObjectGraph(ObjectGraphOpts[vid, at, vd, ad]{
		IsVertexDataEqual: func(a, b vd) bool {
			return a == b
		},
		IsArcDataEqual: func(a, b ad) bool {
			return a == b
		},
		Observer: o,
	})
	return g, o
}

func arc2id(vertexID vid, arcType at) ArcToID[vid, at] { //nolint:unparam
	return ArcToID[vid, at]{To: vertexID, ArcType: arcType}
}

func newArcSet(arcs ...ArcToID[vid, at]) ArcSet[vid, at] {
	res := ArcSet[vid, at]{}

	for _, a := range arcs {
		res[a] = struct{}{}
	}

	return res
}
