// Copyright ©2014 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package topo

import (
	"reflect"
	"slices"
	"testing"

	"gonum.org/v1/gonum/graph"
	"gonum.org/v1/gonum/graph/simple"
	"gonum.org/v1/gonum/internal/order"
)

type interval struct{ start, end int }

var tarjanTests = []struct {
	g []intset

	ambiguousOrder []interval
	want           [][]int64

	sortedLength      int
	unorderableLength int
	sortable          bool
}{
	{
		g: []intset{
			0: linksTo(1),
			1: linksTo(2, 7),
			2: linksTo(3, 6),
			3: linksTo(4),
			4: linksTo(2, 5),
			6: linksTo(3, 5),
			7: linksTo(0, 6),
		},

		want: [][]int64{
			{5},
			{2, 3, 4, 6},
			{0, 1, 7},
		},

		sortedLength:      1,
		unorderableLength: 2,
		sortable:          false,
	},
	{
		g: []intset{
			0: linksTo(1, 2, 3),
			1: linksTo(2),
			2: linksTo(3),
			3: linksTo(1),
		},

		want: [][]int64{
			{1, 2, 3},
			{0},
		},

		sortedLength:      1,
		unorderableLength: 1,
		sortable:          false,
	},
	{
		g: []intset{
			0: linksTo(1),
			1: linksTo(0, 2),
			2: linksTo(1),
		},

		want: [][]int64{
			{0, 1, 2},
		},

		sortedLength:      0,
		unorderableLength: 1,
		sortable:          false,
	},
	{
		g: []intset{
			0: linksTo(1),
			1: linksTo(2, 3),
			2: linksTo(4, 5),
			3: linksTo(4, 5),
			4: linksTo(6),
			5: nil,
			6: nil,
		},

		// Node pairs (2, 3) and (4, 5) are not
		// relatively orderable within each pair.
		ambiguousOrder: []interval{
			{0, 3}, // This includes node 6 since it only needs to be before 4 in topo sort.
			{3, 5},
		},
		want: [][]int64{
			{6}, {5}, {4}, {3}, {2}, {1}, {0},
		},

		sortedLength: 7,
		sortable:     true,
	},
	{
		g: []intset{
			0: linksTo(1),
			1: linksTo(2, 3, 4),
			2: linksTo(0, 3),
			3: linksTo(4),
			4: linksTo(3),
		},

		// SCCs are not relatively ordable.
		ambiguousOrder: []interval{
			{0, 2},
		},
		want: [][]int64{
			{0, 1, 2},
			{3, 4},
		},

		sortedLength:      0,
		unorderableLength: 2,
		sortable:          false,
	},
}

func TestSort(t *testing.T) {
	for i, test := range tarjanTests {
		g := simple.NewDirectedGraph()
		for u, e := range test.g {
			// Add nodes that are not defined by an edge.
			if g.Node(int64(u)) == nil {
				g.AddNode(simple.Node(u))
			}
			for v := range e {
				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
			}
		}
		sorted, err := Sort(g)
		var gotSortedLen int
		for _, n := range sorted {
			if n != nil {
				gotSortedLen++
			}
		}
		if gotSortedLen != test.sortedLength {
			t.Errorf("unexpected number of sortable nodes for test %d: got:%d want:%d", i, gotSortedLen, test.sortedLength)
		}
		if err == nil != test.sortable {
			t.Errorf("unexpected sortability for test %d: got error: %v want: nil-error=%t", i, err, test.sortable)
		}
		if err != nil && len(err.(Unorderable)) != test.unorderableLength {
			t.Errorf("unexpected number of unorderable nodes for test %d: got:%d want:%d", i, len(err.(Unorderable)), test.unorderableLength)
		}
	}
}

func TestTarjanSCC(t *testing.T) {
	for i, test := range tarjanTests {
		g := simple.NewDirectedGraph()
		for u, e := range test.g {
			// Add nodes that are not defined by an edge.
			if g.Node(int64(u)) == nil {
				g.AddNode(simple.Node(u))
			}
			for v := range e {
				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
			}
		}
		gotSCCs := TarjanSCC(g)
		// tarjan.strongconnect does range iteration over maps,
		// so sort SCC members to ensure consistent ordering.
		gotIDs := make([][]int64, len(gotSCCs))
		for i, scc := range gotSCCs {
			gotIDs[i] = make([]int64, len(scc))
			for j, id := range scc {
				gotIDs[i][j] = id.ID()
			}
			slices.Sort(gotIDs[i])
		}
		for _, iv := range test.ambiguousOrder {
			order.BySliceValues(test.want[iv.start:iv.end])
			order.BySliceValues(gotIDs[iv.start:iv.end])
		}
		if !reflect.DeepEqual(gotIDs, test.want) {
			t.Errorf("unexpected Tarjan scc result for %d:\n\tgot:%v\n\twant:%v", i, gotIDs, test.want)
		}
	}
}

var stabilizedSortTests = []struct {
	g []intset

	want []graph.Node
	err  error
}{
	{
		g: []intset{
			0: linksTo(1),
			1: linksTo(2, 7),
			2: linksTo(3, 6),
			3: linksTo(4),
			4: linksTo(2, 5),
			6: linksTo(3, 5),
			7: linksTo(0, 6),
		},

		want: []graph.Node{nil, nil, simple.Node(5)},
		err: Unorderable{
			{simple.Node(0), simple.Node(1), simple.Node(7)},
			{simple.Node(2), simple.Node(3), simple.Node(4), simple.Node(6)},
		},
	},
	{
		g: []intset{
			0: linksTo(1, 2, 3),
			1: linksTo(2),
			2: linksTo(3),
			3: linksTo(1),
		},

		want: []graph.Node{simple.Node(0), nil},
		err: Unorderable{
			{simple.Node(1), simple.Node(2), simple.Node(3)},
		},
	},
	{
		g: []intset{
			0: linksTo(1),
			1: linksTo(0, 2),
			2: linksTo(1),
		},

		want: []graph.Node{nil},
		err: Unorderable{
			{simple.Node(0), simple.Node(1), simple.Node(2)},
		},
	},
	{
		g: []intset{
			0: linksTo(1),
			1: linksTo(2, 3),
			2: linksTo(4, 5),
			3: linksTo(4, 5),
			4: linksTo(6),
			5: nil,
			6: nil,
		},

		want: []graph.Node{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(6)},
		err:  nil,
	},
	{
		g: []intset{
			0: linksTo(1),
			1: linksTo(2, 3, 4),
			2: linksTo(0, 3),
			3: linksTo(4),
			4: linksTo(3),
		},

		want: []graph.Node{nil, nil},
		err: Unorderable{
			{simple.Node(0), simple.Node(1), simple.Node(2)},
			{simple.Node(3), simple.Node(4)},
		},
	},
	{
		g: []intset{
			0: linksTo(1, 2, 3, 4, 5, 6),
			1: linksTo(7),
			2: linksTo(7),
			3: linksTo(7),
			4: linksTo(7),
			5: linksTo(7),
			6: linksTo(7),
			7: nil,
		},

		want: []graph.Node{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(7)},
		err:  nil,
	},
}

func TestSortStabilized(t *testing.T) {
	for i, test := range stabilizedSortTests {
		g := simple.NewDirectedGraph()
		for u, e := range test.g {
			// Add nodes that are not defined by an edge.
			if g.Node(int64(u)) == nil {
				g.AddNode(simple.Node(u))
			}
			for v := range e {
				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
			}
		}
		got, err := SortStabilized(g, nil)
		if !reflect.DeepEqual(got, test.want) {
			t.Errorf("unexpected sort result for test %d: got:%d want:%d", i, got, test.want)
		}
		if !reflect.DeepEqual(err, test.err) {
			t.Errorf("unexpected sort error for test %d: got:%v want:%v", i, err, test.want)
		}
	}
}
