package weed_server

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"testing"
	"time"

	"google.golang.org/grpc"

	"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
	"github.com/seaweedfs/seaweedfs/weed/stats"
	"github.com/seaweedfs/seaweedfs/weed/storage"
	"github.com/seaweedfs/seaweedfs/weed/storage/backend"
	"github.com/seaweedfs/seaweedfs/weed/storage/needle"
	"github.com/seaweedfs/seaweedfs/weed/storage/types"
	"github.com/seaweedfs/seaweedfs/weed/util"
)

const tierTimestampTestBackendName = "tier_timestamp_test.default"

// discardServerStream drops everything sent to it. The embedded
// grpc.ServerStream is nil, so Context is implemented here rather than promoted
// -- the tier RPCs read it to authorize the caller.
type discardServerStream[T any] struct {
	grpc.ServerStream
}

func (s *discardServerStream[T]) Send(*T) error {
	return nil
}

func (s *discardServerStream[T]) Context() context.Context {
	return context.Background()
}

type tierTimestampTestBackend struct {
	root string
}

func (b *tierTimestampTestBackend) ToProperties() map[string]string {
	return map[string]string{"root": b.root}
}

func (b *tierTimestampTestBackend) NewStorageFile(key string, volumeInfo *volume_server_pb.VolumeInfo) backend.BackendStorageFile {
	return &tierTimestampTestBackendFile{
		path:       filepath.Join(b.root, key),
		volumeInfo: volumeInfo,
	}
}

func (b *tierTimestampTestBackend) CopyFile(file *os.File, fn func(progressed int64, percentage float32) error) (key string, size int64, err error) {
	key = "remote.dat"
	fileInfo, err := file.Stat()
	if err != nil {
		return "", 0, err
	}

	output, err := os.Create(filepath.Join(b.root, key))
	if err != nil {
		return "", 0, err
	}
	defer output.Close()

	size, err = io.Copy(output, io.NewSectionReader(file, 0, fileInfo.Size()))
	if err == nil && fn != nil {
		err = fn(size, 100)
	}
	return key, size, err
}

func (b *tierTimestampTestBackend) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error) (size int64, err error) {
	input, err := os.Open(filepath.Join(b.root, key))
	if err != nil {
		return 0, err
	}
	defer input.Close()

	output, err := os.Create(fileName)
	if err != nil {
		return 0, err
	}
	defer output.Close()

	size, err = io.Copy(output, input)
	if err == nil && fn != nil {
		err = fn(size, 100)
	}
	return size, err
}

func (b *tierTimestampTestBackend) DeleteFile(key string) error {
	return os.Remove(filepath.Join(b.root, key))
}

type tierTimestampTestBackendFile struct {
	path       string
	volumeInfo *volume_server_pb.VolumeInfo
}

func (f *tierTimestampTestBackendFile) ReadAt(p []byte, off int64) (int, error) {
	file, err := os.Open(f.path)
	if err != nil {
		return 0, err
	}
	defer file.Close()
	return file.ReadAt(p, off)
}

func (f *tierTimestampTestBackendFile) WriteAt(p []byte, off int64) (int, error) {
	return 0, fmt.Errorf("remote test file is read-only")
}

func (f *tierTimestampTestBackendFile) Truncate(off int64) error {
	return fmt.Errorf("remote test file is read-only")
}

func (f *tierTimestampTestBackendFile) Close() error {
	return nil
}

func (f *tierTimestampTestBackendFile) GetStat() (datSize int64, modTime time.Time, err error) {
	files := f.volumeInfo.GetFiles()
	if len(files) == 0 {
		return 0, time.Time{}, fmt.Errorf("remote file info not found")
	}
	return int64(files[0].GetFileSize()), time.Unix(int64(files[0].GetModifiedTime()), 0), nil
}

func (f *tierTimestampTestBackendFile) Name() string {
	return f.path
}

func (f *tierTimestampTestBackendFile) Sync() error {
	return nil
}

func TestVolumeTierMoveDatPreservesModifiedTime(t *testing.T) {
	dataDir := t.TempDir()
	remoteDir := t.TempDir()
	testBackend := &tierTimestampTestBackend{root: remoteDir}
	backend.BackendStorages[tierTimestampTestBackendName] = testBackend
	t.Cleanup(func() {
		delete(backend.BackendStorages, tierTimestampTestBackendName)
	})

	store := storage.NewStore(
		nil,
		"localhost",
		8080,
		18080,
		"http://localhost:8080",
		"store-id",
		[]string{dataDir},
		[]int32{10},
		[]util.MinFreeSpace{{}},
		"",
		storage.NeedleMapInMemory,
		[]types.DiskType{types.HardDriveType},
		nil,
		0,
		stats.DefaultDiskIOProbeConfig(),
	)
	t.Cleanup(store.Close)

	const volumeId = needle.VolumeId(1)
	if err := store.AddVolume(volumeId, "", storage.NeedleMapInMemory, "000", "", 0, needle.Version3, 0, types.HardDriveType, 0); err != nil {
		t.Fatalf("add volume: %v", err)
	}

	volume := store.GetVolume(volumeId)
	dataFileName := volume.FileName(".dat")
	sourceModifiedTime := time.Unix(1_700_000_000, 0)
	if err := os.Chtimes(dataFileName, sourceModifiedTime, sourceModifiedTime); err != nil {
		t.Fatalf("set source modified time: %v", err)
	}

	// Re-open the data backend so the DiskFile caches the on-disk mtime, the way
	// a volume freshly loaded from disk does.
	volume.DataBackend.Close()
	reopened, err := os.OpenFile(dataFileName, os.O_RDWR, 0644)
	if err != nil {
		t.Fatalf("reopen data file: %v", err)
	}
	volume.DataBackend = backend.NewDiskFile(reopened)

	volumeServer := &VolumeServer{store: store}
	if err := volumeServer.VolumeTierMoveDatToRemote(
		&volume_server_pb.VolumeTierMoveDatToRemoteRequest{
			VolumeId:               uint32(volumeId),
			DestinationBackendName: tierTimestampTestBackendName,
		},
		&discardServerStream[volume_server_pb.VolumeTierMoveDatToRemoteResponse]{},
	); err != nil {
		t.Fatalf("move data to remote: %v", err)
	}

	remoteFiles := volume.GetVolumeInfo().GetFiles()
	if len(remoteFiles) != 1 {
		t.Fatalf("remote file count = %d, want 1", len(remoteFiles))
	}
	if got := remoteFiles[0].GetModifiedTime(); got != uint64(sourceModifiedTime.Unix()) {
		t.Fatalf("remote modified time = %d, want %d", got, sourceModifiedTime.Unix())
	}
	if _, err := os.Stat(dataFileName); !os.IsNotExist(err) {
		t.Fatalf("local data file still exists after upload: %v", err)
	}

	if err := volumeServer.VolumeTierMoveDatFromRemote(
		&volume_server_pb.VolumeTierMoveDatFromRemoteRequest{
			VolumeId: uint32(volumeId),
		},
		&discardServerStream[volume_server_pb.VolumeTierMoveDatFromRemoteResponse]{},
	); err != nil {
		t.Fatalf("move data from remote: %v", err)
	}

	fileInfo, err := os.Stat(dataFileName)
	if err != nil {
		t.Fatalf("stat downloaded data file: %v", err)
	}
	if got := fileInfo.ModTime().Unix(); got != sourceModifiedTime.Unix() {
		t.Fatalf("downloaded modified time = %d, want %d", got, sourceModifiedTime.Unix())
	}
}
