package storage

import (
	"bytes"
	"context"
	"net"
	"os"
	"sync/atomic"
	"testing"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"

	"github.com/seaweedfs/seaweedfs/weed/pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
	"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
)

// countingMaster answers LookupEcVolume with one holder per shard and counts
// how often it was asked.
type countingMaster struct {
	master_pb.UnimplementedSeaweedServer
	lookups atomic.Int64
	// onLookup runs inside the RPC, standing in for whatever else the volume
	// server is doing while it waits on the master.
	onLookup func()
	// failLookup answers with an error instead of a shard map.
	failLookup atomic.Bool
}

func (m *countingMaster) LookupEcVolume(_ context.Context, req *master_pb.LookupEcVolumeRequest) (*master_pb.LookupEcVolumeResponse, error) {
	m.lookups.Add(1)
	if m.onLookup != nil {
		m.onLookup()
	}
	if m.failLookup.Load() {
		return nil, status.Error(codes.Unavailable, "master is having a moment")
	}
	resp := &master_pb.LookupEcVolumeResponse{VolumeId: req.VolumeId}
	for shardId := 0; shardId < erasure_coding.TotalShardsCount; shardId++ {
		resp.ShardIdLocations = append(resp.ShardIdLocations, &master_pb.LookupEcVolumeResponse_EcShardIdLocation{
			ShardId:   uint32(shardId),
			Locations: []*master_pb.Location{{Url: "127.0.0.1:1", GrpcPort: 2}},
		})
	}
	return resp, nil
}

func startCountingMaster(t *testing.T) (*countingMaster, pb.ServerAddress) {
	t.Helper()
	master := &countingMaster{}
	lis, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	srv := grpc.NewServer()
	master_pb.RegisterSeaweedServer(srv, master)
	go srv.Serve(lis)
	t.Cleanup(srv.Stop)
	return master, pb.NewServerAddressWithGrpcPort("127.0.0.1:1", lis.Addr().(*net.TCPAddr).Port)
}

// rewindShardLocationsRefresh ages the cache by d so a test can reach a TTL
// without waiting one out.
func rewindShardLocationsRefresh(ecVolume *erasure_coding.EcVolume, d time.Duration) {
	ecVolume.ShardLocationsLock.Lock()
	ecVolume.ShardLocationsRefreshTime = ecVolume.ShardLocationsRefreshTime.Add(-d)
	ecVolume.ShardLocationsLock.Unlock()
}

// A read that fails against a cached location leaves the map claiming something
// it has just disproved, so the next lookup goes back to the master within
// seconds rather than serving the hole for the rest of a 7-minute window.
func TestCachedLookupEcShardLocationsRefreshesAfterAFailedRead(t *testing.T) {
	master, masterAddr := startCountingMaster(t)
	store := &Store{
		MasterAddress:  masterAddr,
		grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
	}
	ecVolume := &erasure_coding.EcVolume{
		VolumeId:       7,
		ShardLocations: make(map[erasure_coding.ShardId][]pb.ServerAddress),
	}

	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("first lookup: %v", err)
	}
	if got := master.lookups.Load(); got != 1 {
		t.Fatalf("cold cache asked the master %d times, want 1", got)
	}

	// A map nothing has disproved is trusted for minutes, whether or not it is
	// complete -- dropping a shard by hand is not the same as a read failing.
	rewindShardLocationsRefresh(ecVolume, 12*time.Second)
	ecVolume.ShardLocationsLock.Lock()
	delete(ecVolume.ShardLocations, erasure_coding.ShardId(2))
	ecVolume.ShardLocationsLock.Unlock()
	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("second lookup: %v", err)
	}
	if got := master.lookups.Load(); got != 1 {
		t.Errorf("a 12s-old map asked the master %d times, want it left alone", got)
	}

	forgetShardId(ecVolume, erasure_coding.ShardId(3))
	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("lookup after a failed read: %v", err)
	}
	if got := master.lookups.Load(); got != 2 {
		t.Errorf("a map disproved by a failed read asked the master %d times, want 2", got)
	}

	ecVolume.ShardLocationsLock.RLock()
	_, restored := ecVolume.ShardLocations[erasure_coding.ShardId(3)]
	ecVolume.ShardLocationsLock.RUnlock()
	if !restored {
		t.Error("shard 3 is still missing from the map after the refresh")
	}
}

// A failed read buys one prompt re-check, not a lookup on every read that
// follows: the refresh clears the mark and the map is trusted again.
func TestCachedLookupEcShardLocationsSettlesAfterRefresh(t *testing.T) {
	master, masterAddr := startCountingMaster(t)
	store := &Store{
		MasterAddress:  masterAddr,
		grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
	}
	ecVolume := &erasure_coding.EcVolume{
		VolumeId:       8,
		ShardLocations: make(map[erasure_coding.ShardId][]pb.ServerAddress),
	}
	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("first lookup: %v", err)
	}
	rewindShardLocationsRefresh(ecVolume, 12*time.Second)
	forgetShardId(ecVolume, erasure_coding.ShardId(3))
	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("lookup after a failed read: %v", err)
	}
	for i := 0; i < 5; i++ {
		if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
			t.Fatalf("settled lookup: %v", err)
		}
	}
	if got := master.lookups.Load(); got != 2 {
		t.Errorf("asked the master %d times, want 2: one cold, one after the failed read", got)
	}
}

func TestEcShardLocationsTTL(t *testing.T) {
	ecCtx := erasure_coding.NewDefaultECContext("", 1)
	for _, tc := range []struct {
		name       string
		shardCount int
		stale      bool
		want       time.Duration
	}{
		{"complete", ecCtx.Total(), false, 37 * time.Minute},
		{"one shard short", ecCtx.Total() - 1, false, 7 * time.Minute},
		{"below the data shards", ecCtx.DataShards - 1, false, 11 * time.Second},
		{"complete but disproved by a read", ecCtx.Total(), true, 11 * time.Second},
		{"one shard short and disproved", ecCtx.Total() - 1, true, 11 * time.Second},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if got := ecShardLocationsTTL(tc.shardCount, tc.stale, ecCtx); got != tc.want {
				t.Errorf("ttl = %v, want %v", got, tc.want)
			}
		})
	}
}

// oneBadShardPeer serves every shard out of an EC volume's files except one,
// standing in for a shard that has moved off the address the map still names.
type oneBadShardPeer struct {
	volume_server_pb.UnimplementedVolumeServerServer
	baseFileName string
	badShard     erasure_coding.ShardId
}

func (p *oneBadShardPeer) VolumeEcShardRead(req *volume_server_pb.VolumeEcShardReadRequest, stream volume_server_pb.VolumeServer_VolumeEcShardReadServer) error {
	if erasure_coding.ShardId(req.ShardId) == p.badShard {
		return status.Errorf(codes.NotFound, "shard %d is not here", req.ShardId)
	}
	f, err := os.Open(p.baseFileName + erasure_coding.ToExt(int(req.ShardId)))
	if err != nil {
		return err
	}
	defer f.Close()
	data := make([]byte, req.Size)
	if _, err := f.ReadAt(data, req.Offset); err != nil {
		return err
	}
	return stream.Send(&volume_server_pb.VolumeEcShardReadResponse{Data: data, EncodeTsNs: req.EncodeTsNs})
}

// Recovery skips the shard whose direct read failed, so that failure is the one
// thing that never invalidates the map. The read still succeeds off the other
// shards, and the location it just disproved has to be re-checked anyway.
func TestReadOneEcShardIntervalMarksTheMapAfterADirectReadFails(t *testing.T) {
	const badShard = erasure_coding.ShardId(3)
	baseFileName, n := writeEcVolumeFiles(t, t.TempDir(), 7)

	lis, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	srv := grpc.NewServer()
	volume_server_pb.RegisterVolumeServerServer(srv, &oneBadShardPeer{baseFileName: baseFileName, badShard: badShard})
	go srv.Serve(lis)
	t.Cleanup(srv.Stop)

	store := &Store{grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials())}
	ecVolume := &erasure_coding.EcVolume{
		VolumeId:       7,
		ShardLocations: make(map[erasure_coding.ShardId][]pb.ServerAddress),
	}
	seedShardLocations(ecVolume, pb.NewServerAddressWithGrpcPort("127.0.0.1:1", lis.Addr().(*net.TCPAddr).Port))

	interval := erasure_coding.Interval{BlockIndex: int(badShard), Size: 4096, IsLargeBlock: true}
	got := make([]byte, interval.Size)
	if _, err := store.readOneEcShardInterval(n.Id, ecVolume, interval, got); err != nil {
		t.Fatalf("read interval: %v", err)
	}

	ecVolume.ShardLocationsLock.RLock()
	stale := ecVolume.ShardLocationsStale
	_, stillMapped := ecVolume.ShardLocations[badShard]
	ecVolume.ShardLocationsLock.RUnlock()
	if !stale {
		t.Error("a failed direct read left the map trusted, so the moved shard stays hidden until the window expires")
	}
	if !stillMapped {
		t.Error("shard 3 was dropped from the map; the direct read is worth retrying")
	}

	want := make([]byte, len(got))
	f, err := os.Open(baseFileName + erasure_coding.ToExt(int(badShard)))
	if err != nil {
		t.Fatalf("open shard %d: %v", badShard, err)
	}
	defer f.Close()
	if _, err := f.ReadAt(want, 0); err != nil {
		t.Fatalf("read shard %d: %v", badShard, err)
	}
	if !bytes.Equal(got, want) {
		t.Error("the interval recovered from the other shards does not match the shard on disk")
	}
}

// A read that fails while the master is answering has disproved the very map
// that answer is about to install, so the refresh must not clear its mark.
func TestCachedLookupEcShardLocationsKeepsAMarkRaisedDuringTheLookup(t *testing.T) {
	master, masterAddr := startCountingMaster(t)
	store := &Store{
		MasterAddress:  masterAddr,
		grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
	}
	ecVolume := &erasure_coding.EcVolume{
		VolumeId:       9,
		ShardLocations: make(map[erasure_coding.ShardId][]pb.ServerAddress),
	}

	master.onLookup = func() { markShardLocationsStale(ecVolume) }
	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("first lookup: %v", err)
	}
	master.onLookup = nil

	ecVolume.ShardLocationsLock.RLock()
	stale := ecVolume.ShardLocationsStale
	ecVolume.ShardLocationsLock.RUnlock()
	if !stale {
		t.Fatal("the refresh swallowed a mark raised while it was in flight")
	}

	// And the mark shortens the window, so the next read re-checks in seconds
	// rather than trusting a map already disproved.
	rewindShardLocationsRefresh(ecVolume, 12*time.Second)
	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("lookup after the swallowed mark: %v", err)
	}
	if got := master.lookups.Load(); got != 2 {
		t.Errorf("asked the master %d times, want 2", got)
	}
}

// A refresh that never answered cannot be treated as one that did: the mark it
// consumed goes back, or the map it was meant to correct is trusted for its
// whole window on the strength of a lookup that failed.
func TestCachedLookupEcShardLocationsKeepsTheMarkWhenTheLookupFails(t *testing.T) {
	master, masterAddr := startCountingMaster(t)
	store := &Store{
		MasterAddress:  masterAddr,
		grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
	}
	ecVolume := &erasure_coding.EcVolume{
		VolumeId:       10,
		ShardLocations: make(map[erasure_coding.ShardId][]pb.ServerAddress),
	}
	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("first lookup: %v", err)
	}

	rewindShardLocationsRefresh(ecVolume, 12*time.Second)
	markShardLocationsStale(ecVolume)
	master.failLookup.Store(true)
	if err := store.cachedLookupEcShardLocations(ecVolume); err == nil {
		t.Fatal("expected the lookup to fail")
	}

	ecVolume.ShardLocationsLock.RLock()
	stale := ecVolume.ShardLocationsStale
	ecVolume.ShardLocationsLock.RUnlock()
	if !stale {
		t.Error("the failed lookup kept the mark it consumed")
	}

	master.failLookup.Store(false)
	if err := store.cachedLookupEcShardLocations(ecVolume); err != nil {
		t.Fatalf("retry after the failed lookup: %v", err)
	}
	if got := master.lookups.Load(); got != 3 {
		t.Errorf("asked the master %d times, want 3: a failed lookup must not leave the disproved map trusted", got)
	}
}
