// Copyright IBM Corp. 2024, 2026
// SPDX-License-Identifier: BUSL-1.1

package check

import (
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/go-version"
)

type Stream int

const (
	// BothStreams will use both stdout and stderr for performing a check
	BothStreams Stream = iota
	// OnlyStdout will only use stdout for performing a check
	OnlyStdout
	// OnlySterr will only use stderr for performing a check
	OnlyStderr
)

func (s Stream) String() string {
	switch s {
	case BothStreams:
		return "Both streams"
	case OnlyStdout:
		return "Stdout"
	case OnlyStderr:
		return "Stderr"
	}

	panic(fmt.Sprintf("Unknown stream value: %d", s))
}

// Checker represents anything that can be used in conjunction with Assert.
//
// The role of a checker is performing a test on a command's outputs/error, and
// return an error if the test fails.
//
// Note: the Check method is the only required, however during tests the name
// of the checker is printed out in case it fails, so it may be useful to have
// a custom string for this: the `Name() string` method is exactly what to
// implement for this kind of customization.
type Checker interface {
	Check(stdout, stderr string, err error) error
}

func InferName(c Checker) string {
	if c == nil {
		panic("nil checker - malformed test?")
	}

	checkerType := reflect.TypeOf(c)
	_, ok := checkerType.MethodByName("Name")
	if !ok {
		return checkerType.String()
	}

	retVals := reflect.ValueOf(c).MethodByName("Name").Call([]reflect.Value{})
	if len(retVals) != 1 {
		panic(fmt.Sprintf("Name function called - returned %d values. Must be one string only.", len(retVals)))
	}

	return retVals[0].String()
}

func MustSucceed() Checker {
	return mustSucceed{}
}

type mustSucceed struct{}

func (_ mustSucceed) Check(stdout, stderr string, err error) error {
	return err
}

func MustFail() Checker {
	return mustFail{}
}

type mustFail struct{}

func (_ mustFail) Check(stdout, stderr string, err error) error {
	if err == nil {
		return fmt.Errorf("unexpected command success")
	}
	return nil
}

type GrepOpts int

const (
	// Invert the check, i.e. by default an empty grep fails, if this is set, a non-empty grep fails
	GrepInvert GrepOpts = iota
	// Only grep stderr
	GrepStderr
	// Only grep stdout
	GrepStdout
)

// Grep returns a checker that performs a regexp match on the command's output and returns an error if it failed
//
// Note: by default both streams will be checked by the grep
func Grep(expression string, opts ...GrepOpts) Checker {
	pc := PipeChecker{
		name:   fmt.Sprintf("command | grep -E %q", expression),
		stream: BothStreams,
		pipers: []Pipe{
			PipeGrep(expression),
		},
		check: ExpectNonEmptyInput(),
	}
	for _, opt := range opts {
		switch opt {
		case GrepInvert:
			pc.check = ExpectEmptyInput()
		case GrepStderr:
			pc.stream = OnlyStderr
		case GrepStdout:
			pc.stream = OnlyStdout
		}
	}
	return pc
}

func GrepInverted(expression string, opts ...GrepOpts) Checker {
	return Grep(expression, append(opts, GrepInvert)...)
}

type PluginVersionTuple struct {
	Source  string
	Version *version.Version
}

func NewPluginVersionTuple(src, pluginVersion string) PluginVersionTuple {
	ver := version.Must(version.NewVersion(pluginVersion))
	return PluginVersionTuple{
		Source:  src,
		Version: ver,
	}
}

type pluginsUsed struct {
	invert  bool
	plugins []PluginVersionTuple
}

func (pu pluginsUsed) Check(stdout, stderr string, _ error) error {
	var opts []GrepOpts
	if !pu.invert {
		opts = append(opts, GrepInvert)
	}

	var retErr error

	for _, pvt := range pu.plugins {
		// `error` is ignored for Grep, so we can pass in nil
		err := Grep(
			fmt.Sprintf("%s_v%s[^:]+\\\\s*plugin process exited", pvt.Source, pvt.Version.Core()),
			opts...,
		).Check(stdout, stderr, nil)
		if err != nil {
			retErr = multierror.Append(retErr, err)
		}
	}

	return retErr
}

// PluginsUsed is a glorified `Grep` checker that looks for a bunch of plugins
// used from the logs of a packer build or packer validate.
//
// Each tuple passed as parameter is looked for in the logs using Grep
func PluginsUsed(invert bool, plugins ...PluginVersionTuple) Checker {
	return pluginsUsed{
		invert:  invert,
		plugins: plugins,
	}
}

func Dump(t *testing.T) Checker {
	return &dump{t}
}

type dump struct {
	t *testing.T
}

func (d dump) Check(stdout, stderr string, err error) error {
	d.t.Logf("Dumping command result.")
	d.t.Logf("Stdout: %s", stdout)
	d.t.Logf("stderr: %s", stderr)
	return nil
}

type PanicCheck struct{}

func (_ PanicCheck) Check(stdout, stderr string, _ error) error {
	if strings.Contains(stdout, "= PACKER CRASH =") || strings.Contains(stderr, "= PACKER CRASH =") {
		return fmt.Errorf("packer has crashed: this is never normal and should be investigated")
	}
	return nil
}

// CustomCheck is meant to be a one-off checker with a user-provided function.
//
// Use this if none of the existing checkers match your use case, and it is not
// reusable/generic enough for use in other tests.
type CustomCheck struct {
	name      string
	checkFunc func(stdout, stderr string, err error) error
}

func (c CustomCheck) Check(stdout, stderr string, err error) error {
	return c.checkFunc(stdout, stderr, err)
}

func (c CustomCheck) Name() string {
	return fmt.Sprintf("custom check - %s", c.name)
}

// LineCountCheck builds a pipe checker to count the number of lines on stdout by default
//
// To change the stream(s) on which to perform the check, you can call SetStream on the
// returned pipe checker.
func LineCountCheck(lines int) *PipeChecker {
	return MkPipeCheck(fmt.Sprintf("line count (%d)", lines), LineCount()).
		SetTester(IntCompare(Eq, lines)).
		SetStream(OnlyStdout)
}
