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/erasure_coding"
)

// --- Normal volume scrub tests ---

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

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

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

	httpClient := framework.NewHTTPClient()
	framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 1, 1), []byte("data-one")))
	framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 2, 2), []byte("data-two")))
	framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 3, 3), []byte("data-three")))

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

	resp, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
		VolumeIds: []uint32{volumeID},
		Mode:      volume_server_pb.VolumeScrubMode_FULL,
	})
	if err != nil {
		t.Fatalf("ScrubVolume FULL on healthy volume failed: %v", err)
	}
	if resp.GetTotalVolumes() != 1 {
		t.Fatalf("expected total_volumes=1, got %d", resp.GetTotalVolumes())
	}
	if resp.GetTotalFiles() != 3 {
		t.Fatalf("expected total_files=3, got %d", resp.GetTotalFiles())
	}
	if len(resp.GetBrokenVolumeIds()) != 0 {
		t.Fatalf("expected no broken volumes, got %v: %v", resp.GetBrokenVolumeIds(), resp.GetDetails())
	}
}

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

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

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

	httpClient := framework.NewHTTPClient()
	framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 1, 1), []byte("important data")))

	framework.CorruptDatFile(t, clusterHarness.BaseDir(), volumeID)

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

	resp, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
		VolumeIds: []uint32{volumeID},
		Mode:      volume_server_pb.VolumeScrubMode_FULL,
	})
	if err != nil {
		t.Fatalf("ScrubVolume FULL on corrupt volume failed: %v", err)
	}
	if len(resp.GetBrokenVolumeIds()) == 0 {
		t.Fatalf("expected broken volume after data corruption, got none")
	}
	if len(resp.GetDetails()) == 0 {
		t.Fatalf("expected error details for corrupt volume")
	}
}

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

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

	const healthyVol = uint32(202)
	const corruptVol = uint32(203)
	framework.AllocateVolume(t, grpcClient, healthyVol, "")
	framework.AllocateVolume(t, grpcClient, corruptVol, "")

	httpClient := framework.NewHTTPClient()
	framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(healthyVol, 1, 1), []byte("healthy")))
	framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(corruptVol, 1, 1), []byte("will corrupt")))

	framework.CorruptIndexFile(t, clusterHarness.BaseDir(), corruptVol)

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

	resp, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
		VolumeIds: []uint32{healthyVol, corruptVol},
		Mode:      volume_server_pb.VolumeScrubMode_INDEX,
	})
	if err != nil {
		t.Fatalf("ScrubVolume INDEX on mixed volumes failed: %v", err)
	}
	if resp.GetTotalVolumes() != 2 {
		t.Fatalf("expected total_volumes=2, got %d", resp.GetTotalVolumes())
	}
	if len(resp.GetBrokenVolumeIds()) != 1 {
		t.Fatalf("expected exactly 1 broken volume, got %v", resp.GetBrokenVolumeIds())
	}
	if resp.GetBrokenVolumeIds()[0] != corruptVol {
		t.Fatalf("expected broken volume %d, got %d", corruptVol, resp.GetBrokenVolumeIds()[0])
	}
}

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

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

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

	_, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
		VolumeIds: []uint32{99999},
		Mode:      volume_server_pb.VolumeScrubMode_FULL,
	})
	if err == nil {
		t.Fatalf("ScrubVolume should fail for missing volume")
	}
}

// --- EC volume scrub tests ---

// ecSetup creates a volume, uploads data, generates EC shards, and mounts all of them.
func ecSetup(t *testing.T, grpcClient volume_server_pb.VolumeServerClient, httpClient *http.Client, volumeURL string, volumeID uint32) {
	t.Helper()
	framework.AllocateVolume(t, grpcClient, volumeID, "")

	fid := framework.NewFileID(volumeID, 1, 0xABCD0001)
	uploadResp := framework.UploadBytes(t, httpClient, volumeURL, fid, []byte("ec-scrub-test-data-payload"))
	_ = framework.ReadAllAndClose(t, uploadResp)
	if uploadResp.StatusCode != http.StatusCreated {
		t.Fatalf("upload expected 201, got %d", uploadResp.StatusCode)
	}

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

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

	allShards := make([]uint32, erasure_coding.TotalShardsCount)
	for i := range allShards {
		allShards[i] = uint32(i)
	}
	_, err = grpcClient.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
		VolumeId:   volumeID,
		Collection: "",
		ShardIds:   allShards,
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsMount all shards failed: %v", err)
	}
}

func TestScrubEcVolumeIndexHealthy(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(210)
	httpClient := framework.NewHTTPClient()
	ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)

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

	resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
		VolumeIds: []uint32{volumeID},
		Mode:      volume_server_pb.VolumeScrubMode_INDEX,
	})
	if err != nil {
		t.Fatalf("ScrubEcVolume INDEX on healthy volume failed: %v", err)
	}
	if resp.GetTotalVolumes() != 1 {
		t.Fatalf("expected total_volumes=1, got %d", resp.GetTotalVolumes())
	}
	if len(resp.GetBrokenVolumeIds()) != 0 {
		t.Fatalf("expected no broken volumes, got %v: %v", resp.GetBrokenVolumeIds(), resp.GetDetails())
	}
}

func TestScrubEcVolumeLocalHealthy(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(211)
	httpClient := framework.NewHTTPClient()
	ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)

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

	resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
		VolumeIds: []uint32{volumeID},
		Mode:      volume_server_pb.VolumeScrubMode_LOCAL,
	})
	if err != nil {
		t.Fatalf("ScrubEcVolume LOCAL on healthy volume failed: %v", err)
	}
	if resp.GetTotalVolumes() != 1 {
		t.Fatalf("expected total_volumes=1, got %d", resp.GetTotalVolumes())
	}
	if resp.GetTotalFiles() != 1 {
		t.Fatalf("expected total_files=1, got %d", resp.GetTotalFiles())
	}
	if len(resp.GetBrokenVolumeIds()) != 0 {
		t.Fatalf("expected no broken volumes, got %v: %v", resp.GetBrokenVolumeIds(), resp.GetDetails())
	}
	if len(resp.GetBrokenShardInfos()) != 0 {
		t.Fatalf("expected no broken shards, got %v", resp.GetBrokenShardInfos())
	}
}

func TestScrubEcVolumeLocalCorruptShard(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(212)
	httpClient := framework.NewHTTPClient()
	ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)

	// Corrupt shard 0 by truncating it.
	framework.CorruptEcShardFile(t, clusterHarness.BaseDir(), volumeID, 0)

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

	resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
		VolumeIds: []uint32{volumeID},
		Mode:      volume_server_pb.VolumeScrubMode_LOCAL,
	})
	if err != nil {
		t.Fatalf("ScrubEcVolume LOCAL on corrupt shard failed: %v", err)
	}
	if len(resp.GetBrokenVolumeIds()) == 0 {
		t.Fatalf("expected broken volume after shard corruption")
	}
	if len(resp.GetBrokenShardInfos()) == 0 {
		t.Fatalf("expected broken shard info after shard corruption")
	}
	// Verify all reported broken shards belong to the corrupted volume.
	for _, si := range resp.GetBrokenShardInfos() {
		if si.GetVolumeId() != volumeID {
			t.Fatalf("broken shard info for unexpected volume %d, want %d", si.GetVolumeId(), volumeID)
		}
	}
}

func TestScrubEcVolumeAutoSelectWithEcPresent(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 volumeIDA = uint32(213)
	const volumeIDB = uint32(214)
	httpClient := framework.NewHTTPClient()
	ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeIDA)
	ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeIDB)

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

	// Auto-select (empty VolumeIds) should find both EC volumes.
	resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
		Mode: volume_server_pb.VolumeScrubMode_INDEX,
	})
	if err != nil {
		t.Fatalf("ScrubEcVolume auto-select failed: %v", err)
	}
	if resp.GetTotalVolumes() < 2 {
		t.Fatalf("expected at least 2 EC volumes via auto-select, got %d", resp.GetTotalVolumes())
	}
	if len(resp.GetBrokenVolumeIds()) != 0 {
		t.Fatalf("expected no broken volumes, got %v", resp.GetBrokenVolumeIds())
	}
}

func TestScrubEcVolumeUnsupportedMode(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(215)
	httpClient := framework.NewHTTPClient()
	ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)

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

	_, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
		VolumeIds: []uint32{volumeID},
		Mode:      volume_server_pb.VolumeScrubMode(99),
	})
	if err == nil {
		t.Fatalf("ScrubEcVolume should fail for unsupported mode")
	}
}

func TestScrubEcVolumeIndexCorruptEcx(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(216)
	framework.AllocateVolume(t, grpcClient, volumeID, "")

	httpClient := framework.NewHTTPClient()
	fid := framework.NewFileID(volumeID, 1, 0xABCD0001)
	uploadResp := framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), fid, []byte("ec-ecx-corrupt-test"))
	_ = framework.ReadAllAndClose(t, uploadResp)
	if uploadResp.StatusCode != http.StatusCreated {
		t.Fatalf("upload expected 201, got %d", uploadResp.StatusCode)
	}

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

	// Generate EC shards (this creates the .ecx file on disk).
	_, err := grpcClient.VolumeEcShardsGenerate(ctx, &volume_server_pb.VolumeEcShardsGenerateRequest{
		VolumeId:   volumeID,
		Collection: "",
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsGenerate failed: %v", err)
	}

	// Corrupt the .ecx file BEFORE mounting, so the corrupted size is loaded.
	framework.CorruptEcxFile(t, clusterHarness.BaseDir(), volumeID)

	// Now mount shards - the ecx file size will reflect the corruption.
	allShards := make([]uint32, erasure_coding.TotalShardsCount)
	for i := range allShards {
		allShards[i] = uint32(i)
	}
	_, err = grpcClient.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
		VolumeId:   volumeID,
		Collection: "",
		ShardIds:   allShards,
	})
	if err != nil {
		t.Fatalf("VolumeEcShardsMount failed: %v", err)
	}

	resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
		VolumeIds: []uint32{volumeID},
		Mode:      volume_server_pb.VolumeScrubMode_INDEX,
	})
	if err != nil {
		t.Fatalf("ScrubEcVolume INDEX on corrupt ecx failed: %v", err)
	}
	if len(resp.GetBrokenVolumeIds()) == 0 {
		t.Fatalf("expected broken volume after ECX corruption")
	}
}

// scrubEcUntilLocated runs an EC scrub, retrying while the server is still waiting on
// the master for the shard locations a distributed scrub needs.
func scrubEcUntilLocated(t *testing.T, ctx context.Context, grpcClient volume_server_pb.VolumeServerClient, volumeID uint32, mode volume_server_pb.VolumeScrubMode) *volume_server_pb.ScrubEcVolumeResponse {
	t.Helper()
	deadline := time.Now().Add(60 * time.Second)
	for {
		resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
			VolumeIds: []uint32{volumeID},
			Mode:      mode,
		})
		if err != nil {
			t.Fatalf("ScrubEcVolume %s failed: %v", mode, err)
		}
		waiting := false
		for _, d := range resp.GetDetails() {
			if strings.Contains(d, "failed to locate shard via master grpc") {
				waiting = true
				break
			}
		}
		if !waiting {
			return resp
		}
		if time.Now().After(deadline) {
			t.Fatalf("master never reported EC shard locations for volume %d: %v", volumeID, resp.GetDetails())
		}
		time.Sleep(time.Second)
	}
}

// With a shard gone, FULL cannot read the intervals that lived on it, while READS
// rebuilds them from parity. Either way the missing shard has to be reported: a
// volume that scrubs clean is a volume nobody repairs.
func TestScrubEcVolumeReadsRecoversMissingShard(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(216)
	const missingShard = uint32(0)
	httpClient := framework.NewHTTPClient()
	ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()

	if _, err := grpcClient.VolumeEcShardsUnmount(ctx, &volume_server_pb.VolumeEcShardsUnmountRequest{
		VolumeId: volumeID,
		ShardIds: []uint32{missingShard},
	}); err != nil {
		t.Fatalf("VolumeEcShardsUnmount shard %d failed: %v", missingShard, err)
	}
	if _, err := grpcClient.VolumeEcShardsDelete(ctx, &volume_server_pb.VolumeEcShardsDeleteRequest{
		VolumeId: volumeID,
		ShardIds: []uint32{missingShard},
	}); err != nil {
		t.Fatalf("VolumeEcShardsDelete shard %d failed: %v", missingShard, err)
	}

	fullResp := scrubEcUntilLocated(t, ctx, grpcClient, volumeID, volume_server_pb.VolumeScrubMode_FULL)
	assertOnlyBrokenShard(t, "FULL", fullResp, missingShard)
	if len(fullResp.GetDetails()) == 0 {
		t.Fatalf("FULL should report the needles it could not read")
	}

	readsResp := scrubEcUntilLocated(t, ctx, grpcClient, volumeID, volume_server_pb.VolumeScrubMode_READS)
	assertOnlyBrokenShard(t, "READS", readsResp, missingShard)
	if len(readsResp.GetDetails()) != 0 {
		t.Fatalf("READS should rebuild every needle from parity, got: %v", readsResp.GetDetails())
	}
}

func assertOnlyBrokenShard(t *testing.T, mode string, resp *volume_server_pb.ScrubEcVolumeResponse, shardID uint32) {
	t.Helper()
	infos := resp.GetBrokenShardInfos()
	if len(infos) != 1 || infos[0].GetShardId() != shardID {
		t.Fatalf("%s reported broken shards %v, want only shard %d (details: %v)", mode, infos, shardID, resp.GetDetails())
	}
}
