package dag

import (
	"fmt"
	"testing"

	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
)

func TestCheckLoop(t *testing.T) {
	test := func(names []string, expected bool) {
		d := NewDag()
		root := names[0]
		n := d.NewOrGetNode(root)
		for i := 1; i < len(names); i++ {
			name := names[i]
			n = n.SafeAddDependNode(name)
		}
		looped := d.CheckLoop(root)
		if looped != expected {
			t.Fatal(fmt.Errorf("case %v CheckLoop shoud be `%v` but got `%v`", names, expected, looped))
		}
	}

	test([]string{"a"}, false)
	test([]string{"a", "b"}, false)
	test([]string{"a", "b", "c"}, false)
	test([]string{"a", "a"}, true)
	test([]string{"a", "a", "b"}, true)
	test([]string{"a", "b", "a"}, true)
	test([]string{"a", "b", "c", "a"}, true)
	test([]string{"a", "b", "a", "c"}, true)
}

func TestDagNodeAddDependNode(t *testing.T) {
	t.Run("add new node", func(t *testing.T) {
		d := NewDag()
		root := d.SafeNewNode("root")
		child, err := root.AddDependNode("child")
		if err != nil {
			t.Errorf("AddDependNode() error = %v", err)
		}
		if child.Name != "child" {
			t.Errorf("child.Name = %s, want child", child.Name)
		}
	})

	t.Run("duplicate child", func(t *testing.T) {
		d := NewDag()
		root := d.SafeNewNode("root")
		_, _ = root.AddDependNode("child")
		_, err := root.AddDependNode("child")
		if err == nil {
			t.Error("AddDependNode() duplicate should return error")
		}
	})

	t.Run("safe add multiple", func(t *testing.T) {
		d := NewDag()
		root := d.SafeNewNode("root")
		root.SafeAddDependNodes("a", "b", "c")
		if len(root.Children) != 3 {
			t.Errorf("len(Children) = %d, want 3", len(root.Children))
		}
	})
}

func TestDagNodeAssignAction(t *testing.T) {
	t.Run("assign action", func(t *testing.T) {
		d := NewDag()
		node := d.SafeNewNode("test-action")
		action := &actions.MockActionBase{ActionName: "test-action"}
		err := node.AssignAction(action, nil)
		if err != nil {
			t.Errorf("AssignAction() error = %v", err)
		}
		if node.DagAction == nil {
			t.Error("DagAction is nil after AssignAction()")
		}
	})

	t.Run("name mismatch", func(t *testing.T) {
		d := NewDag()
		node := d.SafeNewNode("node-name")
		action := &actions.MockActionBase{ActionName: "action-name"}
		err := node.AssignAction(action, nil)
		if err == nil {
			t.Error("AssignAction() with mismatched names should return error")
		}
	})

	t.Run("duplicate assign", func(t *testing.T) {
		d := NewDag()
		node := d.SafeNewNode("action")
		action := &actions.MockActionBase{ActionName: "action"}
		_ = node.AssignAction(action, nil)
		err := node.AssignAction(action, nil)
		if err == nil {
			t.Error("AssignAction() duplicate should return error")
		}
	})
}

func TestDagActionId(t *testing.T) {
	t.Run("without preset args", func(t *testing.T) {
		action := &actions.MockActionBase{ActionName: "test"}
		da := NewDagAction(action, nil)
		if da.Id() != "test" {
			t.Errorf("Id() = %s, want test", da.Id())
		}
	})

	t.Run("with preset args", func(t *testing.T) {
		action := &actions.MockActionBase{ActionName: "test"}
		args := actions.ActionArgs{"key": actions.ActionArgString("value")}
		da := NewDagAction(action, args)
		if da.Id() == "test" {
			t.Error("Id() with args should include args hash")
		}
		if da.Id() == "" {
			t.Error("Id() should not be empty")
		}
	})
}

func TestDagActionInvoke(t *testing.T) {
	t.Run("merge args", func(t *testing.T) {
		action := &actions.MockActionBase{ActionName: "test"}
		presetArgs := actions.ActionArgs{"preset": actions.ActionArgString("value")}
		da := NewDagAction(action, presetArgs)

		runtimeArgs := actions.ActionArgs{"runtime": actions.ActionArgString("value2")}
		err := da.Invoke(runtimeArgs)
		if err != nil {
			t.Errorf("Invoke() error = %v", err)
		}
	})
}

func TestDagGetReadyActionTasks(t *testing.T) {
	t.Run("empty dag", func(t *testing.T) {
		d := NewDag()
		ready, waiting, err := d.GetReadyActionTasks("nonexistent", nil)
		if err != nil {
			t.Errorf("GetReadyActionTasks() error = %v", err)
		}
		if len(ready) != 0 || len(waiting) != 0 {
			t.Error("empty dag should return empty lists")
		}
	})

	t.Run("all ready", func(t *testing.T) {
		d := NewDag()
		actionA := &actions.MockActionBase{ActionName: "a"}
		actionB := &actions.MockActionBase{ActionName: "b"}
		d.SafeNewNode("root")
		nodeA := d.SafeNewNode("a")
		nodeB := d.SafeNewNode("b")
		_ = nodeA.AssignAction(actionA, nil)
		_ = nodeB.AssignAction(actionB, nil)
		d["root"].SafeAddDependNodes("a", "b")

		isDoneFunc := func(action *DagAction) (bool, error) { return false, nil }
		ready, waiting, err := d.GetReadyActionTasks("root", isDoneFunc)
		if err != nil {
			t.Errorf("GetReadyActionTasks() error = %v", err)
		}
		if len(ready) != 2 {
			t.Errorf("ready count = %d, want 2", len(ready))
		}
		if len(waiting) != 0 {
			t.Errorf("waiting count = %d, want 0", len(waiting))
		}
	})

	t.Run("some done", func(t *testing.T) {
		d := NewDag()
		actionA := &actions.MockActionBase{ActionName: "a"}
		actionB := &actions.MockActionBase{ActionName: "b"}
		d.SafeNewNode("root")
		nodeA := d.SafeNewNode("a")
		nodeB := d.SafeNewNode("b")
		_ = nodeA.AssignAction(actionA, nil)
		_ = nodeB.AssignAction(actionB, nil)
		d["root"].SafeAddDependNodes("a", "b")

		isDoneFunc := func(action *DagAction) (bool, error) {
			return action.Action.Name() == "a", nil
		}
		ready, _, err := d.GetReadyActionTasks("root", isDoneFunc)
		if err != nil {
			t.Errorf("GetReadyActionTasks() error = %v", err)
		}
		for _, r := range ready {
			if r.Action.Name() == "a" {
				t.Error("done action 'a' should not be in ready list")
			}
		}
	})
}

func TestDagIterate(t *testing.T) {
	t.Run("iterate all nodes", func(t *testing.T) {
		d := NewDag()
		d.SafeNewNode("root").SafeAddDependNodes("a", "b")
		d["a"].SafeAddDependNode("c")

		visited := make(map[string]bool)
		err := d.Iterate("root", func(name string, node *DagNode) error {
			visited[name] = true
			return nil
		})
		if err != nil {
			t.Errorf("Iterate() error = %v", err)
		}
		expected := []string{"root", "a", "b", "c"}
		for _, name := range expected {
			if !visited[name] {
				t.Errorf("node %s not visited", name)
			}
		}
	})

	t.Run("nonexistent root", func(t *testing.T) {
		d := NewDag()
		err := d.Iterate("nonexistent", nil)
		if err == nil {
			t.Error("Iterate() with nonexistent root should return error")
		}
	})
}

func TestDagGetNotFoundNodes(t *testing.T) {
	t.Run("all nodes exist", func(t *testing.T) {
		d := NewDag()
		d.SafeNewNode("root").SafeAddDependNodes("a", "b")
		notFound := d.GetNotFoundNodes("root")
		if len(notFound) != 0 {
			t.Errorf("GetNotFoundNodes() = %v, want empty", notFound)
		}
	})

	t.Run("missing nodes", func(t *testing.T) {
		d := NewDag()
		root := d.SafeNewNode("root")
		root.SafeAddDependNode("missing")
		delete(d, "missing")

		notFound := d.GetNotFoundNodes("root")
		if len(notFound) != 1 || notFound[0] != "missing" {
			t.Errorf("GetNotFoundNodes() = %v, want [missing]", notFound)
		}
	})
}

func TestDagClone(t *testing.T) {
	d := NewDag()
	d.SafeNewNode("root").SafeAddDependNodes("a", "b")

	cloned := d.Clone()
	if len(cloned) != len(d) {
		t.Errorf("Clone() len = %d, want %d", len(cloned), len(d))
	}

	cloned.SafeNewNode("new-node")
	if _, ok := d["new-node"]; ok {
		t.Error("modifying clone should not affect original")
	}
}

func TestPlans(t *testing.T) {
	t.Run("register and get", func(t *testing.T) {
		plans := NewPlans()
		plan := NewDag()
		plan.SafeNewNode("root")

		err := plans.RegisterPlan("root", plan)
		if err != nil {
			t.Errorf("RegisterPlan() error = %v", err)
		}

		got := plans.GetAndCloneDagByRoot("root")
		if got == nil {
			t.Error("GetAndCloneDagByRoot() returned nil")
		}
	})

	t.Run("duplicate register", func(t *testing.T) {
		plans := NewPlans()
		plan := NewDag()
		plan.SafeNewNode("root")

		_ = plans.RegisterPlan("root", plan)
		err := plans.RegisterPlan("root", plan)
		if err == nil {
			t.Error("RegisterPlan() duplicate should return error")
		}
	})

	t.Run("nonexistent plan", func(t *testing.T) {
		plans := NewPlans()
		got := plans.GetAndCloneDagByRoot("nonexistent")
		if got != nil {
			t.Error("GetAndCloneDagByRoot() for nonexistent should return nil")
		}
	})

	t.Run("safe register panics on duplicate", func(t *testing.T) {
		plans := NewPlans()
		plan := NewDag()
		plan.SafeNewNode("root")

		plans.SafeRegisterPlan("root", plan)

		defer func() {
			if r := recover(); r == nil {
				t.Error("SafeRegisterPlan() duplicate should panic")
			}
		}()
		plans.SafeRegisterPlan("root", plan)
	})

	t.Run("clone is independent", func(t *testing.T) {
		plans := NewPlans()
		plan := NewDag()
		plan.SafeNewNode("root")
		plans.SafeRegisterPlan("root", plan)

		cloned := plans.GetAndCloneDagByRoot("root")
		cloned.SafeNewNode("new-node")

		if _, ok := plan["new-node"]; ok {
			t.Error("modifying cloned plan should not affect original")
		}
	})
}

func TestDagNodeIsAllDone(t *testing.T) {
	t.Run("no action", func(t *testing.T) {
		d := NewDag()
		node := d.SafeNewNode("test")
		isDone, err := node.IsAllDone(func(action *DagAction) (bool, error) { return true, nil })
		if err != nil {
			t.Errorf("IsAllDone() error = %v", err)
		}
		if !isDone {
			t.Error("node without action should be done")
		}
	})

	t.Run("action not done", func(t *testing.T) {
		d := NewDag()
		node := d.SafeNewNode("test")
		action := &actions.MockActionBase{ActionName: "test"}
		_ = node.AssignAction(action, nil)

		isDone, err := node.IsAllDone(func(action *DagAction) (bool, error) { return false, nil })
		if err != nil {
			t.Errorf("IsAllDone() error = %v", err)
		}
		if isDone {
			t.Error("node with unfinished action should not be done")
		}
	})
}

func TestDagActionsString(t *testing.T) {
	t.Run("empty", func(t *testing.T) {
		var das DagActions
		if das.String() != "[(empty)]" {
			t.Errorf("empty DagActions.String() = %s, want [(empty)]", das.String())
		}
	})

	t.Run("with actions", func(t *testing.T) {
		action := &actions.MockActionBase{ActionName: "test"}
		das := DagActions{NewDagAction(action, nil)}
		if das.String() == "" {
			t.Error("DagActions.String() should not be empty")
		}
	})
}
