// Copyright 2023 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package external

import (
	"context"
	goerrors "errors"
	"io"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/johannesboyne/gofakes3"
	"github.com/johannesboyne/gofakes3/backend/s3mem"
	"github.com/pingcap/errors"
	backuppb "github.com/pingcap/kvproto/pkg/brpb"
	"github.com/pingcap/tidb/br/pkg/membuf"
	"github.com/pingcap/tidb/br/pkg/storage"
	"github.com/pingcap/tidb/pkg/lightning/common"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/rand"
)

// mockExtStore is only used for test.
type mockExtStore struct {
	src []byte
	idx uint64
}

func (s *mockExtStore) Read(p []byte) (n int, err error) {
	// Read from src to p.
	if s.idx >= uint64(len(s.src)) {
		return 0, io.EOF
	}
	n = copy(p, s.src[s.idx:])
	s.idx += uint64(n)
	return n, nil
}

func (s *mockExtStore) Seek(_ int64, _ int) (int64, error) {
	return 0, errors.Errorf("unsupported operation")
}

func (*mockExtStore) Close() error {
	return nil
}

func (s *mockExtStore) GetFileSize() (int64, error) {
	return int64(len(s.src)), nil
}

func TestByteReader(t *testing.T) {
	st, clean := NewS3WithBucketAndPrefix(t, "test", "testprefix")
	defer clean()

	// Prepare
	err := st.WriteFile(context.Background(), "testfile", []byte("abcde"))
	require.NoError(t, err)

	newRsc := func() storage.ExternalFileReader {
		rsc, err := st.Open(context.Background(), "testfile", nil)
		require.NoError(t, err)
		return rsc
	}

	// Test basic next() usage.
	br, err := newByteReader(context.Background(), newRsc(), 3)
	require.NoError(t, err)
	n, bs := br.next(1)
	require.Equal(t, 1, n)
	require.Equal(t, [][]byte{{'a'}}, bs)
	n, bs = br.next(2)
	require.Equal(t, 2, n)
	require.Equal(t, [][]byte{{'b', 'c'}}, bs)
	require.NoError(t, br.Close())

	// Test basic readNBytes() usage.
	br, err = newByteReader(context.Background(), newRsc(), 3)
	require.NoError(t, err)
	x, err := br.readNBytes(2)
	require.NoError(t, err)
	require.Equal(t, 2, len(x))
	require.Equal(t, byte('a'), x[0])
	require.Equal(t, byte('b'), x[1])
	require.NoError(t, br.Close())

	br, err = newByteReader(context.Background(), newRsc(), 3)
	require.NoError(t, err)
	x, err = br.readNBytes(5) // Read all the data.
	require.NoError(t, err)
	require.Equal(t, 5, len(x))
	require.Equal(t, byte('e'), x[4])
	_, err = br.readNBytes(1) // EOF
	require.ErrorIs(t, err, io.EOF)
	require.NoError(t, br.Close())

	br, err = newByteReader(context.Background(), newRsc(), 3)
	require.NoError(t, err)
	_, err = br.readNBytes(7) // EOF
	require.ErrorIs(t, err, io.ErrUnexpectedEOF)

	err = st.WriteFile(context.Background(), "testfile", []byte("abcdef"))
	require.NoError(t, err)

	ms := &mockExtStore{src: []byte("abcdef")}
	br, err = newByteReader(context.Background(), ms, 2)
	require.NoError(t, err)
	x, err = br.readNBytes(3)
	require.NoError(t, err)
	// Pollute mockExtStore to verify if the slice is not affected.
	copy(ms.src, "xyz")
	require.Equal(t, 3, len(x))
	require.Equal(t, byte('c'), x[2])
	require.NoError(t, br.Close())

	ms = &mockExtStore{src: []byte("abcdef")}
	br, err = newByteReader(context.Background(), ms, 2)
	require.NoError(t, err)
	x, err = br.readNBytes(2)
	require.NoError(t, err)
	// Pollute mockExtStore to verify if the slice is not affected.
	copy(ms.src, "xyz")
	require.Equal(t, 2, len(x))
	require.Equal(t, byte('b'), x[1])
	require.NoError(t, br.Close())
}

func TestByteReaderAuxBuf(t *testing.T) {
	ms := &mockExtStore{src: []byte("0123456789")}
	br, err := newByteReader(context.Background(), ms, 1)
	require.NoError(t, err)
	y1, err := br.readNBytes(1)
	require.NoError(t, err)
	require.Equal(t, []byte("0"), y1)
	y2, err := br.readNBytes(2)
	require.NoError(t, err)
	require.Equal(t, []byte("12"), y2)

	y3, err := br.readNBytes(1)
	require.NoError(t, err)
	require.Equal(t, []byte("3"), y3)
	y4, err := br.readNBytes(2)
	require.NoError(t, err)
	require.Equal(t, []byte("45"), y4)
}

func TestUnexpectedEOF(t *testing.T) {
	st, clean := NewS3WithBucketAndPrefix(t, "test", "testprefix")
	defer func() {
		clean()
	}()

	// Prepare
	err := st.WriteFile(context.Background(), "testfile", []byte("0123456789"))
	require.NoError(t, err)

	newRsc := func() storage.ExternalFileReader {
		rsc, err := st.Open(context.Background(), "testfile", nil)
		require.NoError(t, err)
		return rsc
	}

	br, err := newByteReader(context.Background(), newRsc(), 3)
	require.NoError(t, err)
	_, err = br.readNBytes(100)
	require.ErrorIs(t, err, io.ErrUnexpectedEOF)

	br, err = newByteReader(context.Background(), newRsc(), 3)
	require.NoError(t, err)
	_, err = br.readNBytes(100)
	require.ErrorIs(t, err, io.ErrUnexpectedEOF)
}

func TestEmptyContent(t *testing.T) {
	ms := &mockExtStore{src: []byte{}}
	_, err := newByteReader(context.Background(), ms, 100)
	require.ErrorIs(t, err, io.EOF)

	st, clean := NewS3WithBucketAndPrefix(t, "test", "testprefix")
	defer clean()

	// Prepare
	err = st.WriteFile(context.Background(), "testfile", []byte(""))
	require.NoError(t, err)

	newRsc := func() storage.ExternalFileReader {
		rsc, err := st.Open(context.Background(), "testfile", nil)
		require.NoError(t, err)
		return rsc
	}
	_, err = newByteReader(context.Background(), newRsc(), 100)
	require.ErrorIs(t, err, io.EOF)
}

func TestSwitchMode(t *testing.T) {
	seed := time.Now().Unix()
	rand.Seed(uint64(seed))
	t.Logf("seed: %d", seed)
	st := storage.NewMemStorage()
	// Prepare
	ctx := context.Background()
	writer := NewWriterBuilder().
		SetPropSizeDistance(100).
		SetPropKeysDistance(2).
		BuildOneFile(st, "/test", "0")

	err := writer.Init(ctx, 5*1024*1024)
	require.NoError(t, err)

	kvCnt := 1000000
	kvs := make([]common.KvPair, kvCnt)
	for i := 0; i < kvCnt; i++ {
		randLen := rand.Intn(10) + 1
		kvs[i].Key = make([]byte, randLen)
		_, err := rand.Read(kvs[i].Key)
		require.NoError(t, err)
		randLen = rand.Intn(10) + 1
		kvs[i].Val = make([]byte, randLen)
		_, err = rand.Read(kvs[i].Val)
		require.NoError(t, err)
	}

	for _, item := range kvs {
		err := writer.WriteRow(ctx, item.Key, item.Val)
		require.NoError(t, err)
	}

	err = writer.Close(ctx)
	require.NoError(t, err)
	pool := membuf.NewPool()
	ConcurrentReaderBufferSizePerConc = rand.Intn(100) + 1
	kvReader, err := newKVReader(context.Background(), "/test/0/one-file", st, 0, 64*1024)
	require.NoError(t, err)
	kvReader.byteReader.enableConcurrentRead(st, "/test/0/one-file", 100, ConcurrentReaderBufferSizePerConc, pool.NewBuffer())
	modeUseCon := false
	i := 0
	for {
		if rand.Intn(5) == 0 {
			if modeUseCon {
				kvReader.byteReader.switchConcurrentMode(false)
				modeUseCon = false
			} else {
				kvReader.byteReader.switchConcurrentMode(true)
				modeUseCon = true
			}
		}
		key, val, err := kvReader.nextKV()
		if goerrors.Is(err, io.EOF) {
			break
		}
		require.NoError(t, err)
		require.Equal(t, kvs[i].Key, key)
		require.Equal(t, kvs[i].Val, val)
		i++
	}
}

// NewS3WithBucketAndPrefix creates a new S3Storage for testing.
func NewS3WithBucketAndPrefix(t *testing.T, bucketName, prefixName string) (*storage.S3Storage, func()) {
	backend := s3mem.New()
	faker := gofakes3.New(backend)
	ts := httptest.NewServer(faker.Server())
	err := backend.CreateBucket("test")
	require.NoError(t, err)

	config := aws.NewConfig()
	config.WithEndpoint(ts.URL)
	config.WithRegion("region")
	config.WithCredentials(credentials.NewStaticCredentials("dummy-access", "dummy-secret", ""))
	config.WithS3ForcePathStyle(true) // Removes need for subdomain
	svc := s3.New(session.New(), config)

	st := storage.NewS3StorageForTest(svc, &backuppb.S3{
		Region:       "region",
		Bucket:       bucketName,
		Prefix:       prefixName,
		Acl:          "acl",
		Sse:          "sse",
		StorageClass: "sc",
	})
	return st, ts.Close
}
