package volume_server_grpc_test

import (
	"context"
	"net/http"
	"strings"
	"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/needle"
)

// TestEcDecodePreservesDeletedNeedles verifies that needles deleted via
// VolumeEcBlobDelete (recorded in .ecj) are correctly excluded from the
// decoded volume produced by VolumeEcShardsToVolume.
func TestEcDecodePreservesDeletedNeedles(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

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

	const (
		volumeID = uint32(140)
		keyA     = uint64(990020)
		cookieA  = uint32(0xDA001122)
		keyB     = uint64(990021)
		cookieB  = uint32(0xDA003344)
	)

	framework.AllocateVolume(t, client, volumeID, "")

	httpClient := framework.NewHTTPClient()
	fidA := framework.NewFileID(volumeID, keyA, cookieA)
	fidB := framework.NewFileID(volumeID, keyB, cookieB)
	payloadA := []byte("needle-A-should-be-deleted-after-decode")
	payloadB := []byte("needle-B-should-survive-decode")

	// Upload two needles.
	for _, tc := range []struct {
		fid     string
		payload []byte
	}{
		{fidA, payloadA},
		{fidB, payloadB},
	} {
		resp := framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(), tc.fid, tc.payload)
		_ = framework.ReadAllAndClose(t, resp)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("upload %s: expected 201, got %d", tc.fid, resp.StatusCode)
		}
	}

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

	// EC encode.
	_, err := client.VolumeEcShardsGenerate(ctx, &volume_server_pb.VolumeEcShardsGenerateRequest{
		VolumeId: volumeID, Collection: "",
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsGenerate: %v", err)
	}

	// Mount all data shards so the EC volume is usable.
	_, err = client.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
		VolumeId: volumeID, Collection: "",
		ShardIds: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsMount: %v", err)
	}

	// Delete needle A via EC path (writes to .ecj).
	_, err = client.VolumeEcBlobDelete(ctx, &volume_server_pb.VolumeEcBlobDeleteRequest{
		VolumeId: volumeID, Collection: "",
		FileKey: keyA, Version: uint32(needle.GetCurrentVersion()),
	})
	if err != nil {
		t.Fatalf("VolumeEcBlobDelete needle A: %v", err)
	}

	// Unmount the normal volume so decode writes fresh files.
	_, err = client.VolumeUnmount(ctx, &volume_server_pb.VolumeUnmountRequest{VolumeId: volumeID})
	if err != nil {
		t.Fatalf("VolumeUnmount: %v", err)
	}

	// Decode EC shards back to a normal volume.
	_, err = client.VolumeEcShardsToVolume(ctx, &volume_server_pb.VolumeEcShardsToVolumeRequest{
		VolumeId: volumeID, Collection: "",
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsToVolume: %v", err)
	}

	// Re-mount the decoded volume.
	_, err = client.VolumeMount(ctx, &volume_server_pb.VolumeMountRequest{VolumeId: volumeID})
	if err != nil {
		t.Fatalf("VolumeMount: %v", err)
	}

	// Needle A should be gone (deleted via .ecj before decode).
	respA := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(), fidA)
	bodyA := framework.ReadAllAndClose(t, respA)
	if respA.StatusCode >= 500 {
		t.Fatalf("needle A read: server error %d: %s", respA.StatusCode, bodyA)
	}
	if respA.StatusCode != http.StatusNotFound {
		t.Fatalf("needle A should be 404 after decode, got %d", respA.StatusCode)
	}

	// Needle B should still be readable.
	respB := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(), fidB)
	bodyB := framework.ReadAllAndClose(t, respB)
	if respB.StatusCode != http.StatusOK {
		t.Fatalf("needle B read: expected 200, got %d", respB.StatusCode)
	}
	if string(bodyB) != string(payloadB) {
		t.Fatalf("needle B payload mismatch: got %q, want %q", bodyB, payloadB)
	}
}

// TestEcDecodeCollectsEcjFromPeer verifies that .ecj deletion entries from a
// peer server that contributes no new data shards are still collected during
// decode. This is the regression test for the fix in collectEcShards that
// always copies .ecj from every shard location.
//
// Scenario:
//   - Server 0 holds all 10 data shards (decode target).
//   - Server 1 holds a copy of shard 0 (no new shards for server 0).
//   - A needle is deleted ONLY on server 1 (server 0's .ecj is empty).
//   - During decode on server 0, server 1's .ecj must be collected and applied.
func TestEcDecodeCollectsEcjFromPeer(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	cluster := framework.StartMultiVolumeClusterAuto(t, matrix.P1(), 2)
	conn0, client0 := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(0))
	defer conn0.Close()
	conn1, client1 := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(1))
	defer conn1.Close()

	const (
		volumeID = uint32(141)
		keyA     = uint64(990030)
		cookieA  = uint32(0xDB001122)
		keyB     = uint64(990031)
		cookieB  = uint32(0xDB003344)
	)

	// Allocate and upload on server 0.
	framework.AllocateVolume(t, client0, volumeID, "")

	httpClient := framework.NewHTTPClient()
	fidA := framework.NewFileID(volumeID, keyA, cookieA)
	fidB := framework.NewFileID(volumeID, keyB, cookieB)
	payloadB := []byte("needle-B-should-survive-peer-ecj-decode")

	resp := framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(0), fidA, []byte("needle-A-deleted-on-peer"))
	_ = framework.ReadAllAndClose(t, resp)
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("upload A: expected 201, got %d", resp.StatusCode)
	}
	resp = framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(0), fidB, payloadB)
	_ = framework.ReadAllAndClose(t, resp)
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("upload B: expected 201, got %d", resp.StatusCode)
	}

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

	// EC encode on server 0.
	_, err := client0.VolumeEcShardsGenerate(ctx, &volume_server_pb.VolumeEcShardsGenerateRequest{
		VolumeId: volumeID, Collection: "",
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsGenerate on server 0: %v", err)
	}

	// Build the SourceDataNode address for server 0 (format: host:adminPort.grpcPort).
	sourceDataNode := cluster.VolumeAdminAddress(0) + "." +
		strings.Split(cluster.VolumeGRPCAddress(0), ":")[1]

	// Copy shard 0 + ecx + ecj from server 0 → server 1.
	_, err = client1.VolumeEcShardsCopy(ctx, &volume_server_pb.VolumeEcShardsCopyRequest{
		VolumeId:       volumeID,
		Collection:     "",
		SourceDataNode: sourceDataNode,
		ShardIds:       []uint32{0},
		CopyEcxFile:    true,
		CopyEcjFile:    true,
		CopyVifFile:    true,
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsCopy 0→1: %v", err)
	}

	// Mount shard 0 on server 1 so the EC volume can accept deletions.
	_, err = client1.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
		VolumeId: volumeID, Collection: "",
		ShardIds: []uint32{0},
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsMount on server 1: %v", err)
	}

	// Delete needle A on server 1 only (creates .ecj entry on server 1).
	_, err = client1.VolumeEcBlobDelete(ctx, &volume_server_pb.VolumeEcBlobDeleteRequest{
		VolumeId: volumeID, Collection: "",
		FileKey: keyA, Version: uint32(needle.GetCurrentVersion()),
	})
	if err != nil {
		t.Fatalf("VolumeEcBlobDelete needle A on server 1: %v", err)
	}

	// Mount all data shards on server 0 (the decode target).
	_, err = client0.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
		VolumeId: volumeID, Collection: "",
		ShardIds: []uint32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsMount on server 0: %v", err)
	}

	// Collect .ecj from server 1 → server 0 with NO new shard IDs.
	// This is the critical path: server 1 has shard 0 which server 0 already
	// has, so needToCopyShardsInfo would be empty. Before the fix in
	// collectEcShards, this copy would be skipped entirely, losing server 1's
	// deletion entries.
	server1DataNode := cluster.VolumeAdminAddress(1) + "." +
		strings.Split(cluster.VolumeGRPCAddress(1), ":")[1]

	_, err = client0.VolumeEcShardsCopy(ctx, &volume_server_pb.VolumeEcShardsCopyRequest{
		VolumeId:       volumeID,
		Collection:     "",
		SourceDataNode: server1DataNode,
		ShardIds:       []uint32{}, // No new shards — just .ecj.
		CopyEcxFile:    false,
		CopyEcjFile:    true,
		CopyVifFile:    false,
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsCopy .ecj from server 1→0: %v", err)
	}

	// Unmount the normal volume before decode.
	_, err = client0.VolumeUnmount(ctx, &volume_server_pb.VolumeUnmountRequest{VolumeId: volumeID})
	if err != nil {
		t.Fatalf("VolumeUnmount on server 0: %v", err)
	}

	// Decode on server 0. RebuildEcxFile should see needle A's deletion from
	// the .ecj that was collected from server 1.
	_, err = client0.VolumeEcShardsToVolume(ctx, &volume_server_pb.VolumeEcShardsToVolumeRequest{
		VolumeId: volumeID, Collection: "",
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsToVolume on server 0: %v", err)
	}

	// Re-mount the decoded normal volume.
	_, err = client0.VolumeMount(ctx, &volume_server_pb.VolumeMountRequest{VolumeId: volumeID})
	if err != nil {
		t.Fatalf("VolumeMount on server 0: %v", err)
	}

	// Needle A should be gone — its deletion was in server 1's .ecj.
	respA := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(0), fidA)
	bodyA := framework.ReadAllAndClose(t, respA)
	if respA.StatusCode >= 500 {
		t.Fatalf("needle A read: server error %d: %s", respA.StatusCode, bodyA)
	}
	if respA.StatusCode != http.StatusNotFound {
		t.Fatalf("needle A should be 404 (ecj from peer), got %d", respA.StatusCode)
	}

	// Needle B should still be readable.
	respB := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(0), fidB)
	bodyB := framework.ReadAllAndClose(t, respB)
	if respB.StatusCode != http.StatusOK {
		t.Fatalf("needle B read: expected 200, got %d", respB.StatusCode)
	}
	if string(bodyB) != string(payloadB) {
		t.Fatalf("needle B payload mismatch: got %q, want %q", bodyB, payloadB)
	}
}
