package fr32_test

import (
	"bytes"
	"io"
	"math/rand"
	"os"
	"testing"

	"github.com/stretchr/testify/require"

	ffi "github.com/filecoin-project/filecoin-ffi"
	commpffi "github.com/filecoin-project/go-commp-utils/ffiwrapper"
	"github.com/filecoin-project/go-state-types/abi"

	"github.com/filecoin-project/lotus/storage/sealer/fr32"
)

func padFFI(buf []byte) []byte {
	rf, w, _ := commpffi.ToReadableFile(bytes.NewReader(buf), int64(len(buf)))
	tf, _ := os.CreateTemp("/tmp/", "scrb-")

	_, _, _, err := ffi.WriteWithAlignment(abi.RegisteredSealProof_StackedDrg32GiBV1, rf, abi.UnpaddedPieceSize(len(buf)), tf, nil)
	if err != nil {
		panic(err)
	}
	if err := w(); err != nil {
		panic(err)
	}

	if _, err := tf.Seek(io.SeekStart, 0); err != nil { // nolint:staticcheck
		panic(err)
	}

	padded, err := io.ReadAll(tf)
	if err != nil {
		panic(err)
	}

	if err := tf.Close(); err != nil {
		panic(err)
	}

	if err := os.Remove(tf.Name()); err != nil {
		panic(err)
	}

	return padded
}

func TestPadChunkFFI(t *testing.T) {
	testByteChunk := func(b byte) func(*testing.T) {
		return func(t *testing.T) {
			var buf [128]byte
			copy(buf[:], bytes.Repeat([]byte{b}, 127))

			fr32.Pad(buf[:], buf[:])

			expect := padFFI(bytes.Repeat([]byte{b}, 127))

			require.Equal(t, expect, buf[:])
		}
	}

	t.Run("ones", testByteChunk(0xff))
	t.Run("lsb1", testByteChunk(0x01))
	t.Run("msb1", testByteChunk(0x80))
	t.Run("zero", testByteChunk(0x0))
	t.Run("mid", testByteChunk(0x3c))
}

func TestPadChunkRandEqFFI(t *testing.T) {
	for i := 0; i < 200; i++ {
		var input [127]byte
		rand.Read(input[:])

		var buf [128]byte

		fr32.Pad(input[:], buf[:])

		expect := padFFI(input[:])

		require.Equal(t, expect, buf[:])
	}
}

func TestRoundtrip(t *testing.T) {
	testByteChunk := func(b byte) func(*testing.T) {
		return func(t *testing.T) {
			var buf [128]byte
			input := bytes.Repeat([]byte{0x01}, 127)

			fr32.Pad(input, buf[:])

			var out [127]byte
			fr32.Unpad(buf[:], out[:])

			require.Equal(t, input, out[:])
		}
	}

	t.Run("ones", testByteChunk(0xff))
	t.Run("lsb1", testByteChunk(0x01))
	t.Run("msb1", testByteChunk(0x80))
	t.Run("zero", testByteChunk(0x0))
	t.Run("mid", testByteChunk(0x3c))
}

func TestRoundtripChunkRand(t *testing.T) {
	for i := 0; i < 200; i++ {
		var input [127]byte
		rand.Read(input[:])

		var buf [128]byte
		copy(buf[:], input[:])

		fr32.Pad(buf[:], buf[:])

		var out [127]byte
		fr32.Unpad(buf[:], out[:])

		require.Equal(t, input[:], out[:])
	}
}

func TestRoundtrip16MRand(t *testing.T) {
	up := abi.PaddedPieceSize(16 << 20).Unpadded()

	input := make([]byte, up)
	rand.Read(input[:])

	buf := make([]byte, 16<<20)

	fr32.Pad(input, buf)

	out := make([]byte, up)
	fr32.Unpad(buf, out)

	require.Equal(t, input, out)

	ffi := padFFI(input)
	require.Equal(t, ffi, buf)
}

func BenchmarkPadChunk(b *testing.B) {
	var buf [128]byte
	in := bytes.Repeat([]byte{0xff}, 127)

	b.SetBytes(127)

	for i := 0; i < b.N; i++ {
		fr32.Pad(in, buf[:])
	}
}

func BenchmarkChunkRoundtrip(b *testing.B) {
	var buf [128]byte
	copy(buf[:], bytes.Repeat([]byte{0xff}, 127))
	var out [127]byte

	b.SetBytes(127)

	for i := 0; i < b.N; i++ {
		fr32.Pad(buf[:], buf[:])
		fr32.Unpad(buf[:], out[:])
	}
}

func BenchmarkUnpadChunk(b *testing.B) {
	var buf [128]byte
	copy(buf[:], bytes.Repeat([]byte{0xff}, 127))

	fr32.Pad(buf[:], buf[:])
	var out [127]byte

	b.SetBytes(127)
	b.ReportAllocs()

	bs := buf[:]

	for i := 0; i < b.N; i++ {
		fr32.Unpad(bs, out[:])
	}
}

func BenchmarkUnpad16MChunk(b *testing.B) {
	up := abi.PaddedPieceSize(16 << 20).Unpadded()

	var buf [16 << 20]byte

	fr32.Pad(bytes.Repeat([]byte{0xff}, int(up)), buf[:])
	var out [16 << 20]byte

	b.SetBytes(16 << 20)
	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		fr32.Unpad(buf[:], out[:])
	}
}

func BenchmarkPad16MChunk(b *testing.B) {
	up := abi.PaddedPieceSize(16 << 20).Unpadded()

	var buf [16 << 20]byte

	in := bytes.Repeat([]byte{0xff}, int(up))

	b.SetBytes(16 << 20)
	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		fr32.Pad(in, buf[:])
	}
}

func BenchmarkPad1GChunk(b *testing.B) {
	up := abi.PaddedPieceSize(1 << 30).Unpadded()

	var buf [1 << 30]byte

	in := bytes.Repeat([]byte{0xff}, int(up))

	b.SetBytes(1 << 30)
	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		fr32.Pad(in, buf[:])
	}
}

func BenchmarkUnpad1GChunk(b *testing.B) {
	up := abi.PaddedPieceSize(1 << 30).Unpadded()

	var buf [1 << 30]byte

	fr32.Pad(bytes.Repeat([]byte{0xff}, int(up)), buf[:])
	var out [1 << 30]byte

	b.SetBytes(1 << 30)
	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		fr32.Unpad(buf[:], out[:])
	}
}
