// Adapted from https://github.com/philopon/go-toposort under the MIT License
// Original License:
//
// Copyright (c) 2017 Hirotomo Moriwaki
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

package dag_test

import (
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"
	"golang.org/x/exp/slices"

	"github.com/SecurityBrewery/catalyst/dag"
)

type Edge struct {
	From string
	To   string
}

func TestDuplicatedNode(t *testing.T) {
	t.Parallel()

	graph := dag.NewGraph()
	assert.NoError(t, graph.AddNode("a"))
	assert.Error(t, graph.AddNode("a"))
}

func TestWikipedia(t *testing.T) {
	t.Parallel()

	graph := dag.NewGraph()
	assert.NoError(t, graph.AddNodes("2", "3", "5", "7", "8", "9", "10", "11"))

	edges := []Edge{
		{"7", "8"},
		{"7", "11"},

		{"5", "11"},

		{"3", "8"},
		{"3", "10"},

		{"11", "2"},
		{"11", "9"},
		{"11", "10"},

		{"8", "9"},
	}

	for _, e := range edges {
		assert.NoError(t, graph.AddEdge(e.From, e.To))
	}

	result, err := graph.Toposort()
	if err != nil {
		t.Errorf("closed path detected in no closed pathed graph")
	}

	for _, e := range edges {
		if i, j := slices.Index(result, e.From), slices.Index(result, e.To); i > j {
			t.Errorf("dependency failed: not satisfy %v(%v) > %v(%v)", e.From, i, e.To, j)
		}
	}
}

func TestCycle(t *testing.T) {
	t.Parallel()

	graph := dag.NewGraph()
	assert.NoError(t, graph.AddNodes("1", "2", "3"))

	assert.NoError(t, graph.AddEdge("1", "2"))
	assert.NoError(t, graph.AddEdge("2", "3"))
	assert.NoError(t, graph.AddEdge("3", "1"))

	if _, err := graph.Toposort(); err == nil {
		t.Errorf("closed path not detected in closed pathed graph")
	}
}

func TestGraph_GetParents(t *testing.T) {
	t.Parallel()

	type fields struct {
		nodes []string
		edges map[string]string
	}
	type args struct {
		id string
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		want   []string
	}{
		{"parents 2", fields{nodes: []string{"1", "2", "3"}, edges: map[string]string{"1": "2", "2": "3"}}, args{id: "2"}, []string{"1"}},
		{"parents 3", fields{nodes: []string{"1", "2", "3"}, edges: map[string]string{"1": "3", "2": "3"}}, args{id: "3"}, []string{"1", "2"}},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			g := dag.NewGraph()
			for _, node := range tt.fields.nodes {
				assert.NoError(t, g.AddNode(node))
			}
			for from, to := range tt.fields.edges {
				assert.NoError(t, g.AddEdge(from, to))
			}

			if got := g.GetParents(tt.args.id); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("GetParents() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestDAG_AddNode(t *testing.T) {
	t.Parallel()

	dag := dag.NewGraph()

	v := "1"
	assert.NoError(t, dag.AddNode(v))

	assert.Error(t, dag.AddNode(v))
}

func TestDAG_AddEdge(t *testing.T) {
	t.Parallel()

	dag := dag.NewGraph()
	assert.NoError(t, dag.AddNode("0"))
	assert.NoError(t, dag.AddNode("1"))
	assert.NoError(t, dag.AddNode("2"))
	assert.NoError(t, dag.AddNode("3"))

	// add a single edge and inspect the graph
	assert.NoError(t, dag.AddEdge("1", "2"))

	if parents := dag.GetParents("2"); len(parents) != 1 {
		t.Errorf("GetParents(v2) = %d, want 1", len(parents))
	}

	assert.NoError(t, dag.AddEdge("2", "3"))

	_ = dag.AddEdge("0", "1")
}

func TestDAG_GetParents(t *testing.T) {
	t.Parallel()

	dag := dag.NewGraph()
	assert.NoError(t, dag.AddNode("1"))
	assert.NoError(t, dag.AddNode("2"))
	assert.NoError(t, dag.AddNode("3"))
	_ = dag.AddEdge("1", "3")
	_ = dag.AddEdge("2", "3")

	parents := dag.GetParents("3")
	if length := len(parents); length != 2 {
		t.Errorf("GetParents(v3) = %d, want 2", length)
	}
}

func TestDAG_GetDescendants(t *testing.T) {
	t.Parallel()

	dag := dag.NewGraph()
	assert.NoError(t, dag.AddNode("1"))
	assert.NoError(t, dag.AddNode("2"))
	assert.NoError(t, dag.AddNode("3"))
	assert.NoError(t, dag.AddNode("4"))

	assert.NoError(t, dag.AddEdge("1", "2"))
	assert.NoError(t, dag.AddEdge("2", "3"))
	assert.NoError(t, dag.AddEdge("2", "4"))
}

func TestDAG_Topsort(t *testing.T) {
	t.Parallel()

	dag := dag.NewGraph()
	assert.NoError(t, dag.AddNode("1"))
	assert.NoError(t, dag.AddNode("2"))
	assert.NoError(t, dag.AddNode("3"))
	assert.NoError(t, dag.AddNode("4"))

	assert.NoError(t, dag.AddEdge("1", "2"))
	assert.NoError(t, dag.AddEdge("2", "3"))
	assert.NoError(t, dag.AddEdge("2", "4"))

	desc, _ := dag.Toposort()
	assert.Equal(t, desc, []string{"1", "2", "3", "4"})
}

func TestDAG_TopsortStable(t *testing.T) {
	t.Parallel()

	dag := dag.NewGraph()
	assert.NoError(t, dag.AddNode("1"))
	assert.NoError(t, dag.AddNode("2"))
	assert.NoError(t, dag.AddNode("3"))

	assert.NoError(t, dag.AddEdge("1", "2"))
	assert.NoError(t, dag.AddEdge("1", "3"))

	desc, _ := dag.Toposort()
	assert.Equal(t, desc, []string{"1", "2", "3"})
}

func TestDAG_TopsortStable2(t *testing.T) {
	t.Parallel()

	dag := dag.NewGraph()

	assert.NoError(t, dag.AddNodes("block-ioc", "block-iocs", "block-sender", "board", "fetch-iocs", "escalate", "extract-iocs", "mail-available", "search-email-gateway"))
	assert.NoError(t, dag.AddEdge("block-iocs", "block-ioc"))
	assert.NoError(t, dag.AddEdge("block-sender", "extract-iocs"))
	assert.NoError(t, dag.AddEdge("board", "escalate"))
	assert.NoError(t, dag.AddEdge("board", "mail-available"))
	assert.NoError(t, dag.AddEdge("fetch-iocs", "block-iocs"))
	assert.NoError(t, dag.AddEdge("extract-iocs", "fetch-iocs"))
	assert.NoError(t, dag.AddEdge("mail-available", "block-sender"))
	assert.NoError(t, dag.AddEdge("mail-available", "extract-iocs"))
	assert.NoError(t, dag.AddEdge("mail-available", "search-email-gateway"))
	assert.NoError(t, dag.AddEdge("search-email-gateway", "extract-iocs"))

	sorted, err := dag.Toposort()
	assert.NoError(t, err)

	want := []string{"board", "escalate", "mail-available", "block-sender", "search-email-gateway", "extract-iocs", "fetch-iocs", "block-iocs", "block-ioc"}
	assert.Equal(t, want, sorted)
}
