package dump_wal

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"testing"

	"github.com/golang/snappy"
	"github.com/influxdata/influxdb/v2/tsdb/engine/tsm1"
	"github.com/spf13/cobra"
	"github.com/stretchr/testify/require"
)

func Test_DumpWal_No_Args(t *testing.T) {
	params := cmdParams{
		walPaths:    []string{},
		expectErr:   true,
		expectedOut: "requires at least 1 arg(s), only received 0",
	}

	runCommand(t, params)
}

func Test_DumpWal_Bad_Path(t *testing.T) {
	params := cmdParams{
		findDuplicates: false,
		walPaths:       []string{"badpath.wal"},
		expectErr:      true,
		expectedOut:    "open badpath.wal",
	}

	runCommand(t, params)
}

func Test_DumpWal_Wrong_File_Type(t *testing.T) {
	// Creates a temporary .txt file (wrong extension)
	file := newTempWal(t, false, false)

	params := cmdParams{
		walPaths:    []string{file},
		expectedOut: fmt.Sprintf("invalid wal file path, skipping %s", file),
		expectErr:   false,
	}
	runCommand(t, params)
}

func Test_DumpWal_File_Valid(t *testing.T) {
	file := newTempWal(t, true, false)

	params := cmdParams{
		walPaths: []string{file},
		expectedOuts: []string{
			"[write]",
			"cpu,host=A#!~#float 1.1 1",
			"cpu,host=A#!~#int 1i 1",
			"cpu,host=A#!~#bool true 1",
			"cpu,host=A#!~#string \"string\" 1",
			"cpu,host=A#!~#unsigned 10u 5",
		},
	}

	runCommand(t, params)
}

func Test_DumpWal_Find_Duplicates_None(t *testing.T) {
	file := newTempWal(t, true, false)

	params := cmdParams{
		findDuplicates: true,
		walPaths:       []string{file},
		expectedOut:    "No duplicates or out of order timestamps found",
	}

	runCommand(t, params)
}

func Test_DumpWal_Find_Duplicates_Present(t *testing.T) {
	file := newTempWal(t, true, true)

	params := cmdParams{
		findDuplicates: true,
		walPaths:       []string{file},
		expectedOut:    "cpu,host=A#!~#unsigned",
	}

	runCommand(t, params)
}

func newTempWal(t *testing.T, validExt bool, withDuplicate bool) string {
	t.Helper()

	dir := t.TempDir()

	if !validExt {
		file, err := os.CreateTemp(dir, "dumpwaltest*.txt")
		require.NoError(t, err)
		t.Cleanup(func() {
			file.Close()
		})
		return file.Name()
	}

	file, err := os.CreateTemp(dir, "dumpwaltest*"+"."+tsm1.WALFileExtension)
	require.NoError(t, err)
	t.Cleanup(func() {
		file.Close()
	})

	p1 := tsm1.NewValue(10, 1.1)
	p2 := tsm1.NewValue(1, int64(1))
	p3 := tsm1.NewValue(1, true)
	p4 := tsm1.NewValue(1, "string")
	p5 := tsm1.NewValue(5, uint64(10))

	values := map[string][]tsm1.Value{
		"cpu,host=A#!~#float":    {p1},
		"cpu,host=A#!~#int":      {p2},
		"cpu,host=A#!~#bool":     {p3},
		"cpu,host=A#!~#string":   {p4},
		"cpu,host=A#!~#unsigned": {p5},
	}

	if withDuplicate {
		p6 := tsm1.NewValue(1, uint64(70))
		values = map[string][]tsm1.Value{
			"cpu,host=A#!~#unsigned": {p5, p6},
		}
	}

	// Write to WAL File
	writeWalFile(t, file, values)

	return file.Name()
}

func writeWalFile(t *testing.T, file *os.File, vals map[string][]tsm1.Value) {
	t.Helper()

	e := &tsm1.WriteWALEntry{Values: vals}
	b, err := e.Encode(nil)
	require.NoError(t, err)

	w := tsm1.NewWALSegmentWriter(file)
	err = w.Write(e.Type(), snappy.Encode(nil, b))
	require.NoError(t, err)

	err = w.Flush()
	require.NoError(t, err)

	err = file.Sync()
	require.NoError(t, err)
}

type cmdParams struct {
	findDuplicates   bool
	walPaths         []string
	expectedOut      string
	expectedOuts     []string
	expectErr        bool
	expectExactEqual bool
}

func initCommand(t *testing.T, params cmdParams) *cobra.Command {
	t.Helper()

	// Creates new command and sets args
	cmd := NewDumpWALCommand()

	allArgs := params.walPaths
	if params.findDuplicates {
		allArgs = append(allArgs, "--find-duplicates")
	}

	cmd.SetArgs(allArgs)

	return cmd
}

func getOutput(t *testing.T, cmd *cobra.Command) []byte {
	t.Helper()

	b := bytes.NewBufferString("")
	cmd.SetOut(b)
	cmd.SetErr(b)
	require.NoError(t, cmd.Execute())

	out, err := io.ReadAll(b)
	require.NoError(t, err)

	return out
}

func runCommand(t *testing.T, params cmdParams) {
	t.Helper()

	cmd := initCommand(t, params)

	if params.expectErr {
		require.Contains(t, cmd.Execute().Error(), params.expectedOut)
		return
	}

	// Get output
	out := getOutput(t, cmd)

	// Check output
	if params.expectExactEqual {
		require.Equal(t, string(out), params.expectedOut)
		return
	}

	if params.expectedOut != "" {
		require.Contains(t, string(out), params.expectedOut)
	} else {
		for _, output := range params.expectedOuts {
			require.Contains(t, string(out), output)
		}
	}
}
