// Copyright ©2015 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"
)

var undirectedCyclesInTests = []struct {
	g    []intset
	want [][][]int64
}{
	{
		g: []intset{
			0:  linksTo(1, 2),
			1:  linksTo(2, 4, 5, 9),
			2:  linksTo(4, 7, 9),
			3:  linksTo(5),
			4:  linksTo(8),
			5:  linksTo(7, 8),
			6:  nil,
			7:  nil,
			8:  nil,
			9:  nil,
			10: linksTo(11, 12),
			11: linksTo(12),
			12: nil,
		},
		want: [][][]int64{
			{
				{0, 1, 2, 0},
				{1, 2, 7, 5, 1},
				{1, 2, 9, 1},
				{1, 4, 8, 5, 1},
				{2, 4, 8, 5, 7, 2},
				{10, 11, 12, 10},
			},
			{
				{0, 1, 2, 0},
				{1, 2, 4, 1},
				{1, 2, 7, 5, 1},
				{1, 2, 9, 1},
				{1, 4, 8, 5, 1},
				{10, 11, 12, 10},
			},
			{
				{0, 1, 2, 0},
				{1, 2, 4, 1},
				{1, 2, 9, 1},
				{1, 4, 8, 5, 1},
				{2, 4, 8, 5, 7, 2},
				{10, 11, 12, 10},
			},
			{
				{0, 1, 2, 0},
				{1, 2, 4, 1},
				{1, 2, 7, 5, 1},
				{1, 2, 9, 1},
				{2, 4, 8, 5, 7, 2},
				{10, 11, 12, 10},
			},
		},
	},
}

func TestUndirectedCyclesIn(t *testing.T) {
	for i, test := range undirectedCyclesInTests {
		g := simple.NewUndirectedGraph()
		g.AddNode(simple.Node(-10)) // Make sure we test graphs with sparse IDs.
		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)})
			}
		}
		cycles := UndirectedCyclesIn(g)
		var got [][]int64
		if cycles != nil {
			got = make([][]int64, len(cycles))
		}
		// Canonicalise the cycles.
		for j, c := range cycles {
			ids := make([]int64, len(c))
			for k, n := range canonicalise(c[:len(c)-1]) {
				ids[k] = n.ID()
			}
			ids[len(ids)-1] = ids[0]
			got[j] = ids
		}
		order.BySliceValues(got)
		var matched bool
		for _, want := range test.want {
			if reflect.DeepEqual(got, want) {
				matched = true
				break
			}
		}
		if !matched {
			t.Errorf("unexpected paton result for %d:\n\tgot:%#v\n\twant from:%#v", i, got, test.want)
		}
	}
}

// canonicalise returns the cycle path c cyclicly permuted such that
// the first element has the lowest ID and then conditionally
// reversed so that the second element has the lowest possible
// neighbouring ID.
// c lists each node only once - the final node must not be a
// reiteration of the first node.
func canonicalise(c []graph.Node) []graph.Node {
	if len(c) < 2 {
		return c
	}
	idx := 0
	min := c[0].ID()
	for i, n := range c[1:] {
		if id := n.ID(); id < min {
			idx = i + 1
			min = id
		}
	}
	if idx != 0 {
		c = append(c[idx:], c[:idx]...)
	}
	if c[len(c)-1].ID() < c[1].ID() {
		slices.Reverse(c[1:])
	}
	return c
}
