// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

//go:build !root

package core

import (
	"context"
	"fmt"
	"os"
	"reflect"
	"sync"
	"testing"

	"github.com/purpleidea/mgmt/lang/funcs"
	"github.com/purpleidea/mgmt/lang/interfaces"
	"github.com/purpleidea/mgmt/lang/types"
	"github.com/purpleidea/mgmt/util"
	"github.com/purpleidea/mgmt/util/errwrap"

	"github.com/davecgh/go-spew/spew"
	"github.com/kylelemons/godebug/pretty"
)

func TestPureFuncExec0(t *testing.T) {
	type test struct { // an individual test
		name     string
		funcname string
		args     []types.Value
		fail     bool
		expect   types.Value
	}
	testCases := []test{}

	//{
	//	testCases = append(testCases, test{
	//		name: "",
	//		funcname: "",
	//		args: []types.Value{
	//		},
	//		fail: false,
	//		expect: nil,
	//	})
	//}
	{
		testCases = append(testCases, test{
			name:     "strings.to_lower 0",
			funcname: "strings.to_lower",
			args: []types.Value{
				&types.StrValue{
					V: "HELLO",
				},
			},
			fail: false,
			expect: &types.StrValue{
				V: "hello",
			},
		})
	}
	{
		testCases = append(testCases, test{
			name:     "datetime.now fail",
			funcname: "datetime.now",
			args:     nil,
			fail:     true,
			expect:   nil,
		})
	}
	// TODO: run unification in PureFuncExec if it makes sense to do so...
	//{
	//	testCases = append(testCases, test{
	//		name:     "len 0",
	//		funcname: "len",
	//		args: []types.Value{
	//			&types.StrValue{
	//				V: "Hello, world!",
	//			},
	//		},
	//		fail: false,
	//		expect: &types.IntValue{
	//			V: 13,
	//		},
	//	})
	//}

	names := []string{}
	for index, tc := range testCases { // run all the tests
		if tc.name == "" {
			t.Errorf("test #%d: not named", index)
			continue
		}
		if util.StrInList(tc.name, names) {
			t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
			continue
		}
		names = append(names, tc.name)

		//if index != 3 { // hack to run a subset (useful for debugging)
		//if (index != 20 && index != 21) {
		//if tc.name != "nil" {
		//	continue
		//}

		t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
			name, funcname, args, fail, expect := tc.name, tc.funcname, tc.args, tc.fail, tc.expect

			t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name)

			f, err := funcs.Lookup(funcname)
			if err != nil {
				t.Errorf("test #%d: func lookup failed with: %+v", index, err)
				return
			}

			result, err := funcs.PureFuncExec(f, args)

			if !fail && err != nil {
				t.Errorf("test #%d: func failed with: %+v", index, err)
				return
			}
			if fail && err == nil {
				t.Errorf("test #%d: func passed, expected fail", index)
				return
			}
			if !fail && result == nil {
				t.Errorf("test #%d: func output was nil", index)
				return
			}

			if reflect.DeepEqual(result, expect) {
				return
			}

			// double check because DeepEqual is different since the func exists
			diff := pretty.Compare(result, expect)
			if diff == "" { // bonus
				return
			}
			t.Errorf("test #%d: result did not match expected", index)
			// TODO: consider making our own recursive print function
			t.Logf("test #%d:   actual: \n\n%s\n", index, spew.Sdump(result))
			t.Logf("test #%d: expected: \n\n%s", index, spew.Sdump(expect))

			// more details, for tricky cases:
			diffable := &pretty.Config{
				Diffable:          true,
				IncludeUnexported: true,
				//PrintStringers: false,
				//PrintTextMarshalers: false,
				//SkipZeroFields: false,
			}
			t.Logf("test #%d:   actual: \n\n%s\n", index, diffable.Sprint(result))
			t.Logf("test #%d: expected: \n\n%s", index, diffable.Sprint(expect))
			t.Logf("test #%d: diff:\n%s", index, diff)
		})
	}
}

// Step is used for the timeline in tests.
type Step interface {
	Action() error
	Expect() error
}

type manualStep struct {
	action func() error
	expect func() error

	exit       chan struct{}      // exit signal, set by test harness
	argch      chan []types.Value // send new inputs, set by test harness
	valueptrch chan int           // incoming values, set by test harness
	results    []types.Value      // all values, set by test harness
}

func (obj *manualStep) Action() error {
	return obj.action()
}
func (obj *manualStep) Expect() error {
	return obj.expect()
}

// NewManualStep creates a new manual step with an action and an expect test.
func NewManualStep(action, expect func() error) Step {
	return &manualStep{
		action: action,
		expect: expect,
	}
}

// NewSendInputs sends a list of inputs to the running function to populate it.
// If you send the wrong input signature, then you'll cause a failure. Testing
// this kind of failure is not a goal of these tests, since the unification code
// is meant to guarantee we always send the correct type signature.
func NewSendInputs(inputs []types.Value) Step {
	return &sendInputsStep{
		inputs: inputs,
	}
}

type sendInputsStep struct {
	inputs []types.Value

	exit  chan struct{}      // exit signal, set by test harness
	argch chan []types.Value // send new inputs, set by test harness
	//valueptrch chan int // incoming values, set by test harness
	//results []types.Value // all values, set by test harness
}

func (obj *sendInputsStep) Action() error {
	select {
	case obj.argch <- obj.inputs:
		return nil
	case <-obj.exit:
		return fmt.Errorf("exit called")
	}
}

func (obj *sendInputsStep) Expect() error { return nil }

// NewWaitForNSeconds waits this many seconds for new values from the stream. It
// can timeout if it gets bored of waiting.
func NewWaitForNSeconds(number int, timeout int) Step {
	return &waitAmountStep{
		timer:   number, // timer seconds
		timeout: timeout,
	}
}

// NewWaitForNValues waits for this many values from the stream. It can timeout
// if it gets bored of waiting. If you request more values than can be produced,
// then it will block indefinitely if there's no timeout.
func NewWaitForNValues(number int, timeout int) Step {
	return &waitAmountStep{
		count:   number, // count values
		timeout: timeout,
	}
}

// waitAmountStep waits for either a count of N values, or a timer of N seconds,
// or both. It also accepts a timeout which will cause it to error.
// TODO: have the timeout timer be overall instead of per step!
type waitAmountStep struct {
	count   int // nth count (set to a negative value to disable)
	timer   int // seconds (set to a negative value to disable)
	timeout int // seconds to fail after

	exit chan struct{} // exit signal, set by test harness
	//argch chan []types.Value // send new inputs, set by test harness
	valueptrch chan int      // incoming values, set by test harness
	results    []types.Value // all values, set by test harness
}

func (obj *waitAmountStep) Action() error {
	count := 0
	ticked := false // did we get the timer event?

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	ticker := util.TimeAfterOrBlockCtx(ctx, obj.timer)
	if obj.timer < 0 { // disable timer
		ticked = true
	}

	for {
		if count >= obj.count { // got everything we wanted
			if ticked {
				break
			}
		}
		select {
		case <-obj.exit:
			return fmt.Errorf("exit called")

		case <-util.TimeAfterOrBlock(obj.timeout):
			// TODO: make this overall instead of re-running it each time
			return fmt.Errorf("waited too long for a value")

		case n, ok := <-obj.valueptrch: // read output
			if !ok {
				return fmt.Errorf("unexpected close")
			}
			count++
			_ = n // this is the index of the value we're at

		case <-ticker: // received the timer event
			ticker = nil
			ticked = true
			if obj.count > -1 {
				break
			}
		}
	}
	return nil
}
func (obj *waitAmountStep) Expect() error { return nil }

// NewRangeExpect passes in an expect function which will receive the entire
// range of values ever received. This stream (list) of values can be matched on
// however you like.
func NewRangeExpect(fn func([]types.Value) error) Step {
	return &rangeExpectStep{
		fn: fn,
	}
}

type rangeExpectStep struct {
	fn func([]types.Value) error

	// TODO: we could pass exit to the expect fn if we wanted in the future
	exit chan struct{} // exit signal, set by test harness
	//argch chan []types.Value // send new inputs, set by test harness
	//valueptrch chan int // incoming values, set by test harness
	results []types.Value // all values, set by test harness
}

func (obj *rangeExpectStep) Action() error { return nil }

func (obj *rangeExpectStep) Expect() error {
	results := []types.Value{}
	for _, v := range obj.results { // copy
		value := v.Copy()
		results = append(results, value)
	}
	return obj.fn(results) // run with a copy
}

// vog is a helper function to produce mcl values from golang equivalents that
// is only safe in tests because it panics on error.
func vog(i interface{}) types.Value {
	v, err := types.ValueOfGolang(i)
	if err != nil {
		panic(fmt.Sprintf("unexpected error in vog: %+v", err))
	}
	return v
}

// rcopy is a helper to copy a list of types.Value structs.
func rcopy(input []types.Value) []types.Value {
	result := []types.Value{}
	for i := range input {
		x := input[i].Copy()
		result = append(result, x)
	}
	return result
}

// TestLiveFuncExec0 runs a live execution timeline on a function stream. It is
// very useful for testing function streams.
// FIXME: if the function returns a different type than what is specified by its
// signature, we might block instead of returning a useful error.
func TestLiveFuncExec0(t *testing.T) {
	type args struct {
		argv []types.Value
		next func() // specifies we're ready for the next set of inputs
	}

	type test struct { // an individual test
		name     string
		hostname string // in case we want to simulate a hostname
		funcname string

		// TODO: this could be a generator that keeps pushing out steps until it's done!
		timeline []Step
		expect   func() error // function to check for expected state
		startup  func() error // function to run as startup
		cleanup  func() error // function to run as cleanup
	}

	timeout := -1 // default timeout (block) if not specified elsewhere
	testCases := []test{}
	{
		count := 5
		timeline := []Step{
			NewWaitForNValues(count, timeout), // get 5 values
			// pass in a custom validation function
			NewRangeExpect(func(args []types.Value) error {
				//fmt.Printf("range: %+v\n", args) // debugging
				if len(args) < count {
					return fmt.Errorf("no args found")
				}
				// check for increasing ints (ideal delta == 1)
				x := args[0].Int()
				for i := 1; i < count; i++ {
					if args[i].Int()-x < 1 {
						return fmt.Errorf("range jumps: %+v", args)
					}
					if args[i].Int()-x != 1 {
						// if this fails, travis is just slow
						return fmt.Errorf("timing error: %+v", args)
					}
					x = args[i].Int()
				}
				return nil
			}),
			//NewWaitForNSeconds(5, timeout), // not needed
		}

		testCases = append(testCases, test{
			name:     "simple func",
			hostname: "", // not needed for this func
			funcname: "datetime.now",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		timeline := []Step{
			NewSendInputs([]types.Value{
				vog("helloXworld"),
				vog("X"), // split by this
			}),

			NewWaitForNValues(1, timeout), // more than 1 blocks here

			// pass in a custom validation function
			NewRangeExpect(func(args []types.Value) error {
				//fmt.Printf("range: %+v\n", args) // debugging
				if c := len(args); c != 1 {
					return fmt.Errorf("wrong args count, got: %d", c)
				}
				if args[0].Type().Kind != types.KindList {
					return fmt.Errorf("expected list, got: %+v", args[0])
				}
				if err := vog([]string{"hello", "world"}).Cmp(args[0]); err != nil {
					return errwrap.Wrapf(err, "got different expected value: %+v", args[0])
				}
				return nil
			}),
		}

		testCases = append(testCases, test{
			name:     "simple pure func",
			hostname: "", // not needed for this func
			funcname: "strings.split",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		p := "/tmp/somefiletoread"
		content := "hello world!\n"
		timeline := []Step{
			NewSendInputs([]types.Value{
				vog(p),
			}),

			NewWaitForNValues(1, timeout), // more than 1 blocks here
			NewWaitForNSeconds(5, 10),     // wait longer just to be sure

			// pass in a custom validation function
			NewRangeExpect(func(args []types.Value) error {
				//fmt.Printf("range: %+v\n", args) // debugging
				if c := len(args); c != 1 {
					return fmt.Errorf("wrong args count, got: %d", c)
				}
				if args[0].Type().Kind != types.KindStr {
					return fmt.Errorf("expected str, got: %+v", args[0])
				}
				if err := vog(content).Cmp(args[0]); err != nil {
					return errwrap.Wrapf(err, "got different expected value: %+v", args[0])
				}
				return nil
			}),
		}

		testCases = append(testCases, test{
			name:     "readfile",
			hostname: "", // not needed for this func
			funcname: "os.readfile",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return os.WriteFile(p, []byte(content), 0666) },
			cleanup:  func() error { return os.Remove(p) },
		})
	}
	names := []string{}
	for index, tc := range testCases { // run all the tests
		if tc.name == "" {
			t.Errorf("test #%d: not named", index)
			continue
		}
		if util.StrInList(tc.name, names) {
			t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
			continue
		}
		names = append(names, tc.name)
		t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
			hostname, funcname, timeline, expect, startup, cleanup := tc.hostname, tc.funcname, tc.timeline, tc.expect, tc.startup, tc.cleanup

			t.Logf("\n\ntest #%d: func: %+v\n", index, funcname)
			defer t.Logf("test #%d: done!", index)

			handle, err := funcs.Lookup(funcname) // get function...
			if err != nil {
				t.Errorf("test #%d: func lookup failed with: %+v", index, err)
				return
			}
			sig := handle.Info().Sig
			if sig.Kind != types.KindFunc {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: must be kind func", index)
				return
			}
			if sig.HasUni() {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: func contains unification vars", index)
				return
			}

			if buildableFunc, ok := handle.(interfaces.BuildableFunc); ok {
				if _, err := buildableFunc.Build(sig); err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: can't build function: %v", index, err)
					return
				}
			}

			// run validate!
			if err := handle.Validate(); err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: could not validate Func: %+v", index, err)
				return
			}

			input := make(chan types.Value)  // we close this when we're done
			output := make(chan types.Value) // we create it, func closes it

			debug := testing.Verbose() // set via the -test.v flag to `go test`
			logf := func(format string, v ...interface{}) {
				t.Logf(fmt.Sprintf("test #%d: func: ", index)+format, v...)
			}
			init := &interfaces.Init{
				Hostname: hostname,
				Input:    input,
				Output:   output,
				World:    nil, // TODO: add me somehow!
				Debug:    debug,
				Logf:     logf,
			}

			t.Logf("test #%d: running startup()", index)
			if err := startup(); err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: could not startup: %+v", index, err)
				return
			}

			// run init
			t.Logf("test #%d: running Init", index)
			if err := handle.Init(init); err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: could not init func: %+v", index, err)
				return
			}
			defer func() {
				t.Logf("test #%d: running cleanup()", index)
				if err := cleanup(); err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: could not cleanup: %+v", index, err)
				}
			}()

			wg := &sync.WaitGroup{}
			defer wg.Wait() // if we return early

			argch := make(chan []types.Value)
			errch := make(chan error)
			close1 := make(chan struct{})
			close2 := make(chan struct{})
			kill1 := make(chan struct{})
			kill2 := make(chan struct{})
			//kill3 := make(chan struct{}) // future use
			exit := make(chan struct{})

			mutex := &sync.RWMutex{}
			results := []types.Value{}          // all values received so far
			valueptrch := make(chan int)        // which Nth value are we at?
			killTimeline := make(chan struct{}) // ask timeline to exit

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			// wait for close signals
			wg.Add(1)
			go func() {
				defer wg.Done()
				defer close(errch) // last one turns out the lights
				select {
				case <-close1:
				}
				select {
				case <-close2:
				}
			}()

			// wait for kill signals
			wg.Add(1)
			go func() {
				defer wg.Done()
				select {
				case <-exit:
				case <-kill1:
				case <-kill2:
					//case <-kill3: // future use
				}
				close(killTimeline) // main kill signal for tl
			}()

			// run the stream
			wg.Add(1)
			go func() {
				defer wg.Done()
				defer close(close1)
				if debug {
					logf("Running func")
				}
				err := handle.Stream(ctx) // sends to output chan
				t.Logf("test #%d: stream exited with: %+v", index, err)
				if debug {
					logf("Exiting func")
				}
				if err == nil {
					return
				}
				// we closed with an error...
				select {
				case errch <- errwrap.Wrapf(err, "problem streaming func"):
				}
			}()

			// read from incoming args and send to input channel
			wg.Add(1)
			go func() {
				defer wg.Done()
				defer close(close2)
				defer close(input) // close input when done
				if argch == nil {  // no args
					return
				}
				si := &types.Type{
					// input to functions are structs
					Kind: types.KindStruct,
					Map:  handle.Info().Sig.Map,
					Ord:  handle.Info().Sig.Ord,
				}
				t.Logf("test #%d: func has sig: %s", index, si)

				// TODO: should this be a select with an exit signal?
				for args := range argch { // chan
					st := types.NewStruct(si)
					count := 0
					for i, arg := range args {
						//name := util.NumToAlpha(i) // assume (incorrectly) for now...
						name := handle.Info().Sig.Ord[i]          // better
						if err := st.Set(name, arg); err != nil { // populate struct
							select {
							case errch <- errwrap.Wrapf(err, "struct set failure"):
							}
							close(kill1) // unblock tl and cause fail
							return
						}
						count++
					}
					if count != len(si.Map) { // expect this number
						select {
						case errch <- fmt.Errorf("struct field count is wrong"):
						}
						close(kill1) // unblock tl and cause fail
						return
					}

					t.Logf("test #%d: send to func: %s", index, args)
					select {
					case input <- st: // send to function (must not block)
					case <-close1: // unblock the input send in case stream closed
						select {
						case errch <- fmt.Errorf("stream closed early"):
						}
					}
				}
			}()

			// run timeline
			wg.Add(1)
			go func() {
				t.Logf("test #%d: executing timeline", index)
				defer wg.Done()
			Timeline:
				for ix, step := range timeline {
					select {
					case <-killTimeline:
						break Timeline
					default:
						// pass
					}

					mutex.RLock()
					// magic setting of important values...
					if s, ok := step.(*manualStep); ok {
						s.exit = killTimeline      // kill signal
						s.argch = argch            // send inputs here
						s.valueptrch = valueptrch  // receive value ptr
						s.results = rcopy(results) // all results as array
					}
					if s, ok := step.(*sendInputsStep); ok {
						s.exit = killTimeline
						s.argch = argch
						//s.valueptrch = valueptrch
						//s.results = rcopy(results)
					}
					if s, ok := step.(*waitAmountStep); ok {
						s.exit = killTimeline
						//s.argch = argch
						s.valueptrch = valueptrch
						s.results = rcopy(results)
					}
					if s, ok := step.(*rangeExpectStep); ok {
						s.exit = killTimeline
						//s.argch = argch
						//s.valueptrch = valueptrch
						s.results = rcopy(results)
					}
					mutex.RUnlock()

					t.Logf("test #%d: step(%d)...", index, ix)
					if err := step.Action(); err != nil {
						t.Errorf("test #%d: FAIL", index)
						t.Errorf("test #%d: step(%d) action failed: %s", index, ix, err.Error())
						break
					}
					if err := step.Expect(); err != nil {
						t.Errorf("test #%d: FAIL", index)
						t.Errorf("test #%d: step(%d) expect failed: %s", index, ix, err.Error())
						break
					}
				}
				t.Logf("test #%d: timeline finished", index)
				close(argch)

				t.Logf("test #%d: running cancel", index)
				cancel()
			}()

			// read everything
			counter := 0
		Loop:
			for {
				select {
				case value, ok := <-output: // read from channel
					if !ok {
						output = nil
						continue Loop // only exit via errch closing!
					}
					t.Logf("test #%d: got from func: %s", index, value)
					// check return type
					if err := handle.Info().Sig.Out.Cmp(value.Type()); err != nil {
						t.Errorf("test #%d: FAIL", index)
						t.Errorf("test #%d: unexpected return type from func: %+v", index, err)
						close(kill2)
						continue Loop // only exit via errch closing!
					}

					mutex.Lock()
					results = append(results, value) // save value
					mutex.Unlock()
					counter++

				case err, ok := <-errch: // handle possible errors
					if !ok {
						break Loop
					}
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: error: %+v", index, err)
					continue Loop // only exit via errch closing!
				}

				// send events to our timeline
				select {
				case valueptrch <- counter: // TODO: send value?

					// TODO: add this sort of thing, but don't block everyone who doesn't read
					//case <-time.After(time.Duration(globalStepReadTimeout) * time.Second):
					//	t.Errorf("test #%d: FAIL", index)
					//	t.Errorf("test #%d: timeline receiver was too slow for value", index)
					//	t.Errorf("test #%d: got(%d): %+v", index, counter, results[counter])
					//	close(kill3) // shut everything down
					//	continue Loop // only exit via errch closing!
				}
			}

			t.Logf("test #%d: waiting for shutdown", index)
			close(exit)
			wg.Wait()

			if err := expect(); err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expect failed: %s", index, err.Error())
				return
			}

			// all done!
		})
	}
}
