package fsm

import (
	"fmt"
	"reflect"
	"sort"
	"testing"

	"codeberg.org/eduVPN/eduvpn-common/internal/test"
)

func TestSortSlice(t *testing.T) {
	cases := []struct {
		In   StateIDSlice
		Want StateIDSlice
	}{
		{
			In:   StateIDSlice{0, 1, 2, 3},
			Want: StateIDSlice{0, 1, 2, 3},
		},
		{
			In:   StateIDSlice{0, 3, 2, 3},
			Want: StateIDSlice{0, 2, 3, 3},
		},
		{
			In:   StateIDSlice{0, 3, 2, 1},
			Want: StateIDSlice{0, 1, 2, 3},
		},
	}

	for _, c := range cases {
		sort.Sort(c.In)
		if !reflect.DeepEqual(c.In, c.Want) {
			t.Fatalf("slice not sorted, in: %v, want: %v", c.In, c.Want)
		}
	}
}

func testFSM() FSM {
	states := States{
		0: State{
			Transitions: []Transition{
				{To: 1},
				{To: 2},
			},
		},
		1: State{
			Transitions: []Transition{},
		},
		2: State{
			Transitions: []Transition{
				{To: 1},
			},
		},
		// isolated state
		3: State{
			Transitions: []Transition{},
		},
	}
	cb := func(StateID, StateID, any) bool {
		return false
	}
	namecb := func(in StateID) string {
		return fmt.Sprintf("Test%d", in)
	}
	return NewFSM(0, states, cb, namecb)
}

func TestCheckTransition(t *testing.T) {
	machine := testFSM()

	cases := []struct {
		In      StateID
		WantErr string
	}{
		{
			In:      1,
			WantErr: "",
		},
		{
			In:      2,
			WantErr: "fsm invalid transition attempt from 'Test1' to 'Test2'",
		},
		// we can always go back to the initial state
		{
			In:      0,
			WantErr: "",
		},
		{
			In:      2,
			WantErr: "",
		},
		{
			In:      3,
			WantErr: "fsm invalid transition attempt from 'Test2' to 'Test3'",
		},
	}

	for _, c := range cases {
		err := machine.CheckTransition(c.In)
		test.AssertError(t, err, c.WantErr)

		// mock a transition
		if err == nil {
			machine.Current = c.In
		}
	}
}

func TestGoTransitionRequired(t *testing.T) {
	machine := testFSM()

	cases := []struct {
		In      StateID
		Handle  bool
		WantErr string
		Data    string
	}{
		{
			In:      1,
			WantErr: "fsm failed transition from 'Test0' to 'Test1', is this required transition handled?",
		},
		{
			In:      1,
			Handle:  true,
			WantErr: "",
		},
		{
			In:      2,
			WantErr: "fsm invalid transition attempt from 'Test1' to 'Test2'",
		},
		// we can always go back to the initial state
		{
			In:      0,
			Handle:  true,
			WantErr: "",
			Data:    "test",
		},
		{
			In:      2,
			Handle:  false,
			WantErr: "fsm failed transition from 'Test0' to 'Test2', is this required transition handled?",
		},
		{
			In:      3,
			WantErr: "fsm invalid transition attempt from 'Test0' to 'Test3'",
		},
	}

	for _, c := range cases {
		curr := machine.Current
		machine.StateCallback = func(_ StateID, newState StateID, data any) bool {
			if c.WantErr == "" && newState != c.In {
				t.Fatalf("new state is not what we want, got: %v, want: %v", newState, c.In)
			}
			v, ok := data.(string)
			if !ok {
				t.Fatal("data is not a string")
			}
			if c.Data != v {
				t.Fatalf("data is not equal, got: %v, want: %v", v, c.Data)
			}
			return c.Handle
		}
		err := machine.GoTransitionRequired(c.In, c.Data)
		test.AssertError(t, err, c.WantErr)

		// mock setting state back if the handle was not successful
		if c.WantErr != "" {
			machine.Current = curr
		}
	}
}

func TestGoTransition(t *testing.T) {
	machine := testFSM()

	cases := []struct {
		In      StateID
		Handle  bool
		Data    string
		WantErr string
	}{
		{
			In:      1,
			Data:    "test",
			WantErr: "",
		},
		// self-loops not allowed
		{
			In:      1,
			WantErr: "fsm invalid transition attempt from 'Test1' to 'Test1'",
		},
		{
			In:      2,
			WantErr: "fsm invalid transition attempt from 'Test1' to 'Test2'",
		},
		// we can always go back to the initial state
		{
			In:      0,
			Handle:  true,
			WantErr: "",
		},
		{
			In:      2,
			Handle:  false,
			WantErr: "",
		},
		{
			In:      3,
			WantErr: "fsm invalid transition attempt from 'Test2' to 'Test3'",
		},
	}

	for _, c := range cases {
		curr := machine.Current
		machine.StateCallback = func(StateID, StateID, any) bool {
			return c.Handle
		}
		machine.StateCallback = func(_ StateID, newState StateID, data any) bool {
			if c.WantErr == "" && newState != c.In {
				t.Fatalf("new state is not what we want, got: %v, want: %v", newState, c.In)
			}
			v, ok := data.(string)
			if !ok {
				t.Fatal("data is not a string")
			}
			if c.Data != v {
				t.Fatalf("data is not equal, got: %v, want: %v", v, c.Data)
			}
			return c.Handle
		}
		var ghandle bool
		var err error
		if c.Data != "" {
			ghandle, err = machine.GoTransitionWithData(c.In, c.Data)
		} else {
			ghandle, err = machine.GoTransition(c.In)
		}
		test.AssertError(t, err, c.WantErr)

		if ghandle != c.Handle {
			t.Fatalf("handled bool not equal, got: %v, want: %v", ghandle, c.Handle)
		}

		// mock setting state back if the handle was not successful
		if c.WantErr != "" {
			machine.Current = curr
		} else if !machine.InState(c.In) {
			t.Fatalf("after successful transition, FSM is not in state: %v", c.In)
		}
	}
}
