package erasure_coding

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

	"github.com/seaweedfs/seaweedfs/test/volume_server/framework"
	"github.com/seaweedfs/seaweedfs/test/volume_server/matrix"
	"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
	"github.com/seaweedfs/seaweedfs/weed/storage/volume_info"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func TestCopyVolumeFilesToWorkerUsesCurrentCompactionRevision(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
	conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
	defer conn.Close()

	const volumeID = uint32(951)
	framework.AllocateVolume(t, grpcClient, volumeID, "")

	httpClient := framework.NewHTTPClient()

	liveFID := framework.NewFileID(volumeID, 1001, 0x1111AAAA)
	liveUploadResp := framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), liveFID, []byte("live-payload-for-ec-copy"))
	_ = framework.ReadAllAndClose(t, liveUploadResp)
	require.Equal(t, http.StatusCreated, liveUploadResp.StatusCode)

	deletedFID := framework.NewFileID(volumeID, 1002, 0x2222BBBB)
	deletedUploadResp := framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), deletedFID, []byte("deleted-payload-for-vacuum"))
	_ = framework.ReadAllAndClose(t, deletedUploadResp)
	require.Equal(t, http.StatusCreated, deletedUploadResp.StatusCode)

	deleteReq, err := http.NewRequest(http.MethodDelete, clusterHarness.VolumeAdminURL()+"/"+deletedFID, nil)
	require.NoError(t, err)
	deleteResp := framework.DoRequest(t, httpClient, deleteReq)
	_ = framework.ReadAllAndClose(t, deleteResp)
	require.Equal(t, http.StatusAccepted, deleteResp.StatusCode)

	compactVolumeOnce(t, grpcClient, volumeID)

	task := NewErasureCodingTask(
		"copy-after-compaction",
		clusterHarness.VolumeServerAddress(),
		volumeID,
		"",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	require.NoError(t, task.markReplicasReadonly(ctx))

	fileStatus, err := task.readSourceVolumeFileStatus(ctx)
	require.NoError(t, err)
	require.Greater(t, fileStatus.GetCompactionRevision(), uint32(0))

	localFiles, err := task.copyVolumeFilesToWorker(ctx, t.TempDir())
	require.NoError(t, err)

	datInfo, err := os.Stat(localFiles["dat"])
	require.NoError(t, err)
	require.Equal(t, int64(fileStatus.GetDatFileSize()), datInfo.Size())

	idxInfo, err := os.Stat(localFiles["idx"])
	require.NoError(t, err)
	require.Equal(t, int64(fileStatus.GetIdxFileSize()), idxInfo.Size())
}

// markReplicasReadonly must persist the readonly mark into the .vif so a
// source-server restart cannot silently reopen the volume to writes that the
// EC shards would not contain.
func TestMarkReplicasReadonlyPersists(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
	conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
	defer conn.Close()

	const volumeID = uint32(954)
	framework.AllocateVolume(t, grpcClient, volumeID, "")

	httpClient := framework.NewHTTPClient()
	fid := framework.NewFileID(volumeID, 3001, 0x4444DDDD)
	resp := framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), fid, []byte("payload-for-readonly-persist"))
	_ = framework.ReadAllAndClose(t, resp)
	require.Equal(t, http.StatusCreated, resp.StatusCode)

	task := NewErasureCodingTask(
		"ec-readonly-persist",
		clusterHarness.VolumeServerAddress(),
		volumeID,
		"",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	require.NoError(t, task.markReplicasReadonly(ctx))

	vifPath := filepath.Join(clusterHarness.BaseDir(), "volume", fmt.Sprintf("%d.vif", volumeID))
	vi, _, found, err := volume_info.MaybeLoadVolumeInfo(vifPath)
	require.NoError(t, err)
	require.True(t, found, "volume must have a .vif after a persisted readonly mark")
	require.True(t, vi.ReadOnly, "markReplicasReadonly must persist ReadOnly=true into the .vif")
}

// The worker-local encode path must stamp the EC ratio and an encode identity
// into the .vif, or the read guard is silently off for worker-encoded volumes.
func TestGenerateEcShardsLocallyStampsEncodeIdentity(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
	conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
	defer conn.Close()

	const volumeID = uint32(952)
	framework.AllocateVolume(t, grpcClient, volumeID, "")

	httpClient := framework.NewHTTPClient()
	fid := framework.NewFileID(volumeID, 2001, 0x3333CCCC)
	resp := framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), fid, []byte("payload-for-ec-encode-identity"))
	_ = framework.ReadAllAndClose(t, resp)
	require.Equal(t, http.StatusCreated, resp.StatusCode)

	task := NewErasureCodingTask(
		"ec-encode-identity",
		clusterHarness.VolumeServerAddress(),
		volumeID,
		"",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	require.NoError(t, task.markReplicasReadonly(ctx))
	localFiles, err := task.copyVolumeFilesToWorker(ctx, t.TempDir())
	require.NoError(t, err)

	shardFiles, err := task.generateEcShardsLocally(localFiles, t.TempDir())
	require.NoError(t, err)

	vifPath := shardFiles["vif"]
	require.NotEmpty(t, vifPath, "generate must produce a .vif")
	vi, _, found, err := volume_info.MaybeLoadVolumeInfo(vifPath)
	require.NoError(t, err)
	require.True(t, found)
	require.NotNil(t, vi.EcShardConfig, "worker .vif must record the EC config")
	require.Greater(t, vi.EcShardConfig.DataShards, uint32(0))
	require.Greater(t, vi.EcShardConfig.ParityShards, uint32(0))
	require.NotZero(t, vi.EcShardConfig.EncodeTsNs, "worker-encoded .vif must carry an encode identity")
}

// A fenced run must stamp the admin-issued generation (t.encodeTsNs) verbatim into
// the distributed .vif, so the generation the worker distributes equals the one it
// later sends on the teardown RPCs and the volume-server fence compares like-for-like.
func TestGenerateEcShardsLocallyUsesAdminGeneration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
	conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
	defer conn.Close()

	const volumeID = uint32(957)
	framework.AllocateVolume(t, grpcClient, volumeID, "")

	httpClient := framework.NewHTTPClient()
	fid := framework.NewFileID(volumeID, 2002, 0x5555EEEE)
	resp := framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), fid, []byte("payload-for-admin-generation"))
	_ = framework.ReadAllAndClose(t, resp)
	require.Equal(t, http.StatusCreated, resp.StatusCode)

	task := NewErasureCodingTask(
		"ec-admin-generation",
		clusterHarness.VolumeServerAddress(),
		volumeID,
		"",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	const adminGeneration int64 = 1_700_000_000_000_000_321
	task.encodeTsNs = adminGeneration

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	require.NoError(t, task.markReplicasReadonly(ctx))
	localFiles, err := task.copyVolumeFilesToWorker(ctx, t.TempDir())
	require.NoError(t, err)

	shardFiles, err := task.generateEcShardsLocally(localFiles, t.TempDir())
	require.NoError(t, err)

	vi, _, found, err := volume_info.MaybeLoadVolumeInfo(shardFiles["vif"])
	require.NoError(t, err)
	require.True(t, found)
	require.NotNil(t, vi.EcShardConfig)
	require.Equal(t, adminGeneration, vi.EcShardConfig.EncodeTsNs, "fenced run must stamp the admin generation verbatim, not a local timestamp")
}

func compactVolumeOnce(t *testing.T, grpcClient volume_server_pb.VolumeServerClient, volumeID uint32) {
	t.Helper()

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	compactStream, err := grpcClient.VacuumVolumeCompact(ctx, &volume_server_pb.VacuumVolumeCompactRequest{
		VolumeId: volumeID,
	})
	require.NoError(t, err)

	for {
		_, err = compactStream.Recv()
		if err == io.EOF {
			break
		}
		require.NoError(t, err)
	}

	_, err = grpcClient.VacuumVolumeCommit(ctx, &volume_server_pb.VacuumVolumeCommitRequest{
		VolumeId: volumeID,
	})
	require.NoError(t, err)

	_, err = grpcClient.VacuumVolumeCleanup(ctx, &volume_server_pb.VacuumVolumeCleanupRequest{
		VolumeId: volumeID,
	})
	require.NoError(t, err)
}
