package task_test

import (
	"bytes"
	"path/filepath"
	"testing"

	"github.com/sebdah/goldie/v2"
	"github.com/stretchr/testify/require"

	"github.com/go-task/task/v3"
	"github.com/go-task/task/v3/internal/experiments"
	"github.com/go-task/task/v3/taskfile/ast"
)

type (
	// A FormatterTestOption is a function that configures an [FormatterTest].
	FormatterTestOption interface {
		applyToFormatterTest(*FormatterTest)
	}
	// A FormatterTest is a test wrapper around a [task.Executor] to make it
	// easy to write tests for the task formatter. See [NewFormatterTest] for
	// information on creating and running FormatterTests. These tests use
	// fixture files to assert whether the result of the output is correct. If
	// Task's behavior has been changed, the fixture files can be updated by
	// running `task gen:fixtures`.
	FormatterTest struct {
		TaskTest
		task           string
		vars           map[string]any
		executorOpts   []task.ExecutorOption
		listOptions    task.ListOptions
		wantSetupError bool
		wantListError  bool
	}
)

// NewFormatterTest sets up a new [task.Executor] with the given options and
// runs a task with the given [FormatterTestOption]s. The output of the task is
// written to a set of fixture files depending on the configuration of the test.
func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) {
	t.Helper()
	tt := &FormatterTest{
		task: "default",
		vars: map[string]any{},
		TaskTest: TaskTest{
			experiments: map[*experiments.Experiment]int{},
		},
	}
	// Apply the functional options
	for _, opt := range opts {
		opt.applyToFormatterTest(tt)
	}
	// Enable any experiments that have been set
	for x, v := range tt.experiments {
		prev := *x
		*x = experiments.Experiment{
			Name:          prev.Name,
			AllowedValues: []int{v},
			Value:         v,
		}
		t.Cleanup(func() {
			*x = prev
		})
	}
	tt.run(t)
}

// Functional options

// WithListOptions sets the list options for the formatter.
func WithListOptions(opts task.ListOptions) FormatterTestOption {
	return &listOptionsTestOption{opts}
}

type listOptionsTestOption struct {
	listOptions task.ListOptions
}

func (opt *listOptionsTestOption) applyToFormatterTest(t *FormatterTest) {
	t.listOptions = opt.listOptions
}

// WithListError tells the test to expect an error when running the formatter.
// A fixture will be created with the output of any errors.
func WithListError() FormatterTestOption {
	return &listErrorTestOption{}
}

type listErrorTestOption struct{}

func (opt *listErrorTestOption) applyToFormatterTest(t *FormatterTest) {
	t.wantListError = true
}

// Helpers

// writeFixtureErrList is a wrapper for writing the output of an error when
// running the formatter to a fixture file.
func (tt *FormatterTest) writeFixtureErrList(
	t *testing.T,
	g *goldie.Goldie,
	err error,
) {
	t.Helper()
	tt.writeFixture(t, g, "err-list", []byte(err.Error()))
}

// run is the main function for running the test. It sets up the task executor,
// runs the task, and writes the output to a fixture file.
func (tt *FormatterTest) run(t *testing.T) {
	t.Helper()
	f := func(t *testing.T) {
		t.Helper()
		var buf bytes.Buffer

		opts := append(
			tt.executorOpts,
			task.WithStdout(&buf),
			task.WithStderr(&buf),
		)

		// Set up the task executor
		e := task.NewExecutor(opts...)

		// Create a golden fixture file for the output
		g := goldie.New(t,
			goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
		)

		// Call setup and check for errors
		if err := e.Setup(); tt.wantSetupError {
			require.Error(t, err)
			tt.writeFixtureErrSetup(t, g, err)
			tt.writeFixtureBuffer(t, g, buf)
			return
		} else {
			require.NoError(t, err)
		}

		// Create the task call
		vars := ast.NewVars()
		for key, value := range tt.vars {
			vars.Set(key, ast.Var{Value: value})
		}

		// Run the formatter and check for errors
		if _, err := e.ListTasks(tt.listOptions); tt.wantListError {
			require.Error(t, err)
			tt.writeFixtureErrList(t, g, err)
			tt.writeFixtureBuffer(t, g, buf)
			return
		} else {
			require.NoError(t, err)
		}

		tt.writeFixtureBuffer(t, g, buf)
	}

	// Run the test (with a name if it has one)
	if tt.name != "" {
		t.Run(tt.name, f)
	} else {
		f(t)
	}
}

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

	NewFormatterTest(t,
		WithExecutorOptions(
			task.WithDir("testdata/label_list"),
		),
		WithListOptions(task.ListOptions{
			ListOnlyTasksWithDescriptions: true,
		}),
	)
}

// task -al case 1: listAll list all tasks
func TestListAllShowsNoDesc(t *testing.T) {
	t.Parallel()

	NewFormatterTest(t,
		WithExecutorOptions(
			task.WithDir("testdata/list_mixed_desc"),
		),
		WithListOptions(task.ListOptions{
			ListAllTasks: true,
		}),
	)
}

// task -al case 2: !listAll list some tasks (only those with desc)
func TestListCanListDescOnly(t *testing.T) {
	t.Parallel()

	NewFormatterTest(t,
		WithExecutorOptions(
			task.WithDir("testdata/list_mixed_desc"),
		),
		WithListOptions(task.ListOptions{
			ListOnlyTasksWithDescriptions: true,
		}),
	)
}

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

	NewFormatterTest(t,
		WithExecutorOptions(
			task.WithDir("testdata/list_desc_interpolation"),
		),
		WithListOptions(task.ListOptions{
			ListOnlyTasksWithDescriptions: true,
		}),
	)
}