package action

import (
	"bufio"
	"bytes"
	"math/rand"
	"os"
	"path/filepath"
	"testing"

	"github.com/gopasspw/gopass/internal/config"
	"github.com/gopasspw/gopass/internal/out"
	"github.com/gopasspw/gopass/pkg/ctxutil"
	"github.com/gopasspw/gopass/tests/gptest"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestBinary(t *testing.T) {
	u := gptest.NewUnitTester(t)

	ctx := config.NewContextInMemory()
	ctx = ctxutil.WithAlwaysYes(ctx, true)
	ctx = ctxutil.WithHidden(ctx, true)

	buf := &bytes.Buffer{}
	out.Stdout = buf
	defer func() {
		out.Stdout = os.Stdout
	}()

	act, err := newMock(ctx, u.StoreDir(""))
	require.NoError(t, err)
	require.NotNil(t, act)
	ctx = act.cfg.WithConfig(ctx)

	require.Error(t, act.Cat(gptest.CliCtx(ctx, t)))
	require.Error(t, act.BinaryCopy(gptest.CliCtx(ctx, t)))
	require.Error(t, act.BinaryMove(gptest.CliCtx(ctx, t)))
	require.Error(t, act.Sum(gptest.CliCtx(ctx, t)))
}

func TestBinaryCat(t *testing.T) {
	tSize := 1024

	u := gptest.NewUnitTester(t)

	ctx := config.NewContextInMemory()
	ctx = ctxutil.WithAlwaysYes(ctx, true)
	ctx = ctxutil.WithHidden(ctx, true)

	buf := &bytes.Buffer{}
	out.Stdout = buf
	stdout = buf
	defer func() {
		out.Stdout = os.Stdout
		stdout = os.Stdout
	}()

	act, err := newMock(ctx, u.StoreDir(""))
	require.NoError(t, err)
	require.NotNil(t, act)
	ctx = act.cfg.WithConfig(ctx)

	infile := filepath.Join(u.Dir, "input.txt")
	writeBinfile(t, infile, tSize)

	t.Run("populate store", func(t *testing.T) {
		require.NoError(t, act.binaryCopy(ctx, gptest.CliCtx(ctx, t), infile, "bar", true))
	})

	t.Run("binary cat bar", func(t *testing.T) {
		require.NoError(t, act.Cat(gptest.CliCtx(ctx, t, "bar")))
	})

	stdinfile := filepath.Join(u.Dir, "stdin")
	t.Run("binary cat baz from stdin", func(t *testing.T) {
		writeBinfile(t, stdinfile, tSize)

		fd, err := os.Open(stdinfile)
		require.NoError(t, err)
		binstdin = fd
		defer func() {
			binstdin = os.Stdin
			_ = fd.Close()
		}()

		require.NoError(t, act.Cat(gptest.CliCtx(ctx, t, "baz")))
	})

	t.Run("compare output", func(t *testing.T) {
		buf, err := os.ReadFile(stdinfile)
		require.NoError(t, err)
		sec, err := act.binaryGet(ctx, "baz")
		require.NoError(t, err)
		assert.Equal(t, string(buf), string(sec))
	})
}

func TestBinaryCatSizes(t *testing.T) {
	u := gptest.NewUnitTester(t)

	ctx := config.NewContextInMemory()
	ctx = ctxutil.WithAlwaysYes(ctx, true)
	ctx = ctxutil.WithHidden(ctx, true)

	buf := &bytes.Buffer{}
	out.Stdout = buf
	stdout = buf
	defer func() {
		out.Stdout = os.Stdout
		stdout = os.Stdout
	}()

	act, err := newMock(ctx, u.StoreDir(""))
	require.NoError(t, err)
	require.NotNil(t, act)
	ctx = act.cfg.WithConfig(ctx)

	for tSize := 1024; tSize < bufio.MaxScanTokenSize*2; tSize += 1024 {
		// cat stdinfile | gopass cat baz
		stdinfile := filepath.Join(u.Dir, "stdin")
		writeBinfile(t, stdinfile, tSize)

		fd, err := os.Open(stdinfile)
		require.NoError(t, err)

		catFn := func() {
			binstdin = fd
			defer func() {
				binstdin = os.Stdin
				_ = fd.Close()
			}()

			require.NoError(t, act.Cat(gptest.CliCtx(ctx, t, "baz")))
		}
		catFn()

		// gopass cat baz and compare output with input, they should match
		buf, err := os.ReadFile(stdinfile)
		require.NoError(t, err)
		sec, err := act.binaryGet(ctx, "baz")
		require.NoError(t, err)

		if string(buf) != string(sec) {
			t.Fatalf("Input and output mismatch at tSize %d", tSize)
		}
		t.Logf("Input and Output match at tSize %d", tSize)
	}
}

func TestBinaryCopy(t *testing.T) {
	u := gptest.NewUnitTester(t)

	ctx := config.NewContextInMemory()
	ctx = ctxutil.WithAlwaysYes(ctx, true)
	ctx = ctxutil.WithHidden(ctx, true)

	buf := &bytes.Buffer{}
	out.Stdout = buf
	defer func() {
		out.Stdout = os.Stdout
	}()

	act, err := newMock(ctx, u.StoreDir(""))
	require.NoError(t, err)
	require.NotNil(t, act)
	ctx = act.cfg.WithConfig(ctx)

	t.Run("copy textfile", func(t *testing.T) {
		defer buf.Reset()

		infile := filepath.Join(u.Dir, "input.txt")
		require.NoError(t, os.WriteFile(infile, []byte("0xDEADBEEF\n"), 0o644))
		require.NoError(t, act.binaryCopy(ctx, gptest.CliCtx(ctx, t), infile, "txt", true))
	})

	infile := filepath.Join(u.Dir, "input.raw")
	outfile := filepath.Join(u.Dir, "output.raw")
	t.Run("copy binary file", func(t *testing.T) {
		defer buf.Reset()

		writeBinfile(t, infile, 1024)
		require.NoError(t, act.binaryCopy(ctx, gptest.CliCtx(ctx, t), infile, "bar", true))
	})

	t.Run("binary copy bar tempdir/bar", func(t *testing.T) {
		defer buf.Reset()
		require.NoError(t, act.BinaryCopy(gptest.CliCtx(ctx, t, "bar", outfile)))
	})

	t.Run("binary copy tempdir/bar tempdir/bar", func(t *testing.T) {
		defer buf.Reset()

		require.Error(t, act.BinaryCopy(gptest.CliCtx(ctx, t, outfile, outfile)))
	})

	t.Run("binary copy bar bar", func(t *testing.T) {
		defer buf.Reset()
		require.Error(t, act.BinaryCopy(gptest.CliCtx(ctx, t, "bar", "bar")))
	})

	t.Run("binary move tempdir/bar bar2", func(t *testing.T) {
		defer buf.Reset()
		require.NoError(t, act.BinaryMove(gptest.CliCtx(ctx, t, outfile, "bar2")))
	})

	t.Run("binary move bar2 tempdir/bar", func(t *testing.T) {
		defer buf.Reset()
		require.NoError(t, act.BinaryMove(gptest.CliCtx(ctx, t, "bar2", outfile)))
	})
}

func TestBinarySum(t *testing.T) {
	u := gptest.NewUnitTester(t)

	ctx := config.NewContextInMemory()
	ctx = ctxutil.WithAlwaysYes(ctx, true)
	ctx = ctxutil.WithHidden(ctx, true)

	buf := &bytes.Buffer{}
	out.Stdout = buf
	defer func() {
		out.Stdout = os.Stdout
	}()

	act, err := newMock(ctx, u.StoreDir(""))
	require.NoError(t, err)
	require.NotNil(t, act)
	ctx = act.cfg.WithConfig(ctx)

	infile := filepath.Join(u.Dir, "input.raw")

	t.Run("populate store", func(t *testing.T) {
		writeBinfile(t, infile, 1024)
		require.NoError(t, act.binaryCopy(ctx, gptest.CliCtx(ctx, t), infile, "bar", true))
	})

	t.Run("binary sum bar", func(t *testing.T) {
		require.NoError(t, act.Sum(gptest.CliCtx(ctx, t, "bar")))
		buf.Reset()
	})
}

func TestBinaryGet(t *testing.T) {
	u := gptest.NewUnitTester(t)

	ctx := config.NewContextInMemory()
	ctx = ctxutil.WithAlwaysYes(ctx, true)
	ctx = ctxutil.WithHidden(ctx, true)

	act, err := newMock(ctx, u.StoreDir(""))
	require.NoError(t, err)
	require.NotNil(t, act)
	ctx = act.cfg.WithConfig(ctx)

	data := []byte("1\n2\n3\n")
	require.NoError(t, act.insertStdin(ctx, "x", data, false))

	out, err := act.binaryGet(ctx, "x")
	require.NoError(t, err)
	assert.Equal(t, data, out)
}

func writeBinfile(t *testing.T, fn string, size int) {
	t.Helper()

	// tests should be predicable
	lr := rand.New(rand.NewSource(42))

	buf := make([]byte, size)
	n, err := lr.Read(buf)
	require.NoError(t, err)
	assert.Equal(t, size, n)
	require.NoError(t, os.WriteFile(fn, buf, 0o644))
}
