package progress_test

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	"github.com/osbuild/image-builder-cli/pkg/progress"
)

func TestProgressNew(t *testing.T) {
	for _, tc := range []struct {
		typ         string
		expected    interface{}
		expectedErr string
	}{
		{"term", &progress.TerminalProgressBar{}, ""},
		{"debug", &progress.DebugProgressBar{}, ""},
		{"verbose", &progress.VerboseProgressBar{}, ""},
		// unknown progress type
		{"bad", nil, `unknown progress type: "bad"`},
	} {
		pb, err := progress.New(tc.typ)
		if tc.expectedErr == "" {
			assert.NoError(t, err)
			assert.Equal(t, reflect.TypeOf(pb), reflect.TypeOf(tc.expected), fmt.Sprintf("[%v] %T not the expected %T", tc.typ, pb, tc.expected))
		} else {
			assert.EqualError(t, err, tc.expectedErr)
		}
	}
}

func TestVerboseProgress(t *testing.T) {
	var buf bytes.Buffer
	restore := progress.MockOsStderr(&buf)
	defer restore()

	// verbose progress never generates progress output
	pbar, err := progress.NewVerboseProgressBar()
	assert.NoError(t, err)
	err = pbar.SetProgress(0, "set-progress", 1, 100)
	assert.NoError(t, err)
	assert.Equal(t, "", buf.String())

	// but it shows the messages
	pbar.SetPulseMsgf("pulse")
	assert.Equal(t, "pulse\n", buf.String())
	buf.Reset()

	pbar.SetMessagef("message")
	assert.Equal(t, "message\n", buf.String())
	buf.Reset()

	pbar.Start()
	assert.Equal(t, "", buf.String())
	pbar.Stop()
	assert.Equal(t, "", buf.String())
}

func TestDebugProgress(t *testing.T) {
	var buf bytes.Buffer
	restore := progress.MockOsStderr(&buf)
	defer restore()

	pbar, err := progress.NewDebugProgressBar()
	assert.NoError(t, err)
	err = pbar.SetProgress(0, "set-progress-msg", 1, 100)
	assert.NoError(t, err)
	assert.Equal(t, "[1 / 100] set-progress-msg\n", buf.String())
	buf.Reset()

	pbar.SetPulseMsgf("pulse-msg")
	assert.Equal(t, "pulse: pulse-msg\n", buf.String())
	buf.Reset()

	pbar.SetMessagef("some-message")
	assert.Equal(t, "msg: some-message\n", buf.String())
	buf.Reset()

	pbar.Start()
	assert.Equal(t, "Start progressbar\n", buf.String())
	buf.Reset()

	pbar.Stop()
	assert.Equal(t, "Stop progressbar\n", buf.String())
	buf.Reset()
}

func TestTermProgress(t *testing.T) {
	var buf bytes.Buffer
	restore := progress.MockOsStderr(&buf)
	defer restore()

	pbar, err := progress.NewTerminalProgressBar()
	assert.NoError(t, err)

	pbar.Start()
	pbar.SetPulseMsgf("pulse-msg")
	pbar.SetMessagef("some-message")
	err = pbar.SetProgress(0, "set-progress-msg", 0, 5)
	assert.NoError(t, err)
	pbar.Stop()
	assert.NoError(t, pbar.(*progress.TerminalProgressBar).Err())

	assert.Contains(t, buf.String(), "[1 / 6] set-progress-msg")
	assert.Contains(t, buf.String(), "[|] pulse-msg\n")
	assert.Contains(t, buf.String(), "Message: some-message\n")
	// check shutdown
	assert.Contains(t, buf.String(), progress.CURSOR_SHOW)
}

func TestProgressNewAutoselect(t *testing.T) {
	for _, tc := range []struct {
		onTerm   bool
		expected interface{}
	}{
		{false, &progress.VerboseProgressBar{}},
		{true, &progress.TerminalProgressBar{}},
	} {
		restore := progress.MockIsattyIsTerminal(func(uintptr) bool {
			return tc.onTerm
		})
		defer restore()

		pb, err := progress.New("auto")
		assert.NoError(t, err)
		assert.Equal(t, reflect.TypeOf(pb), reflect.TypeOf(tc.expected), fmt.Sprintf("[%v] %T not the expected %T", tc.onTerm, pb, tc.expected))
	}
}

func makeFakeOsbuild(t *testing.T, content string) string {
	p := filepath.Join(t.TempDir(), "fake-osbuild")
	err := os.WriteFile(p, []byte("#!/bin/sh\n"+content), 0755)
	assert.NoError(t, err)
	return p
}

func TestRunOSBuildWithProgressErrorReporting(t *testing.T) {
	restore := progress.MockOsStderr(io.Discard)
	defer restore()

	restore = progress.MockOsbuildCmd(makeFakeOsbuild(t, `
>&3 echo '{"message": "osbuild-stage-message"}'

echo osbuild-stdout-output
>&2 echo osbuild-stderr-output
exit 112
`))
	defer restore()

	pbar, err := progress.New("debug")
	assert.NoError(t, err)
	err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, nil)
	assert.EqualError(t, err, `error running osbuild: exit status 112
BuildLog:
osbuild-stage-message
Output:
osbuild-stdout-output
osbuild-stderr-output
`)
}

func TestRunOSBuildWithProgressIncorrectJSON(t *testing.T) {
	signalDeliveredMarkerPath := filepath.Join(t.TempDir(), "sigint-delivered")

	restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, fmt.Sprintf(`
trap 'touch "%s";exit 2' INT

>&3 echo invalid-json

# we cannot sleep infinity here or the shell script trap is never run
while true; do
    sleep 0.1
done
`, signalDeliveredMarkerPath)))
	defer restore()

	pbar, err := progress.New("debug")
	assert.NoError(t, err)
	err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, nil)
	assert.EqualError(t, err, `error parsing osbuild status, please report a bug and try with "--progress=verbose": cannot scan line "invalid-json": invalid character 'i' looking for beginning of value`)

	// ensure the SIGINT got delivered
	var pathExists = func(p string) bool {
		_, err := os.Stat(p)
		return err == nil
	}
	for i := 0; i < 20; i++ {
		time.Sleep(100 * time.Millisecond)
		if pathExists(signalDeliveredMarkerPath) {
			break
		}
	}
	assert.True(t, pathExists(signalDeliveredMarkerPath))
}

func TestRunOSBuildWithBuildlogTerm(t *testing.T) {
	restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, `
echo osbuild-stdout-output
>&2 echo osbuild-stderr-output

# without the sleep this is racy as two different go routines poll
# this does not matter (much) in practise because osbuild output and
# stage output are using the syncedMultiWriter so output is not garbled
sleep 0.1
>&3 echo '{"message": "osbuild-stage-message"}'
`))
	defer restore()

	var fakeStdout, fakeStderr bytes.Buffer
	restore = progress.MockOsStdout(&fakeStdout)
	defer restore()
	restore = progress.MockOsStderr(&fakeStderr)
	defer restore()

	pbar, err := progress.New("term")
	assert.NoError(t, err)

	var buildLog bytes.Buffer
	opts := &progress.OSBuildOptions{
		BuildLog: &buildLog,
	}
	err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, opts)
	assert.NoError(t, err)
	expectedOutput := `osbuild-stdout-output
osbuild-stderr-output
osbuild-stage-message
`
	assert.Equal(t, expectedOutput, buildLog.String())
}

func TestRunOSBuildWithBuildlogVerbose(t *testing.T) {
	restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, `
echo osbuild-stdout-output
>&2 echo osbuild-stderr-output
`))
	defer restore()

	var fakeStdout, fakeStderr bytes.Buffer
	restore = progress.MockOsStdout(&fakeStdout)
	defer restore()
	restore = progress.MockOsStderr(&fakeStderr)
	defer restore()

	pbar, err := progress.New("verbose")
	assert.NoError(t, err)

	var buildLog bytes.Buffer
	opts := &progress.OSBuildOptions{
		BuildLog: &buildLog,
	}
	err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, opts)
	assert.NoError(t, err)
	expectedOutput := `osbuild-stdout-output
osbuild-stderr-output
`
	assert.Equal(t, expectedOutput, buildLog.String())
}

func TestRunOSBuildCacheMaxSize(t *testing.T) {
	fakeOsbuildBinary := makeFakeOsbuild(t, `echo "$@" > "$0".cmdline`)
	restore := progress.MockOsbuildCmd(fakeOsbuildBinary)
	defer restore()

	pbar, err := progress.New("debug")
	assert.NoError(t, err)

	osbuildOpts := &progress.OSBuildOptions{
		CacheMaxSize: 77,
	}
	err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, osbuildOpts)
	assert.NoError(t, err)
	cmdline, err := os.ReadFile(fakeOsbuildBinary + ".cmdline")
	assert.NoError(t, err)
	assert.Contains(t, string(cmdline), "--cache-max-size=77")
}
