package weed_server

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/util"
	"github.com/seaweedfs/seaweedfs/weed/wdclient"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func TestLookupDirectoryEntriesRejectsInvalidRequests(t *testing.T) {
	valid := &filer_pb.LookupDirectoryEntryRequest{Directory: "/batch", Name: "key"}
	tests := []struct {
		name     string
		req      *filer_pb.LookupDirectoryEntriesRequest
		wantCode codes.Code
	}{
		{name: "nil request", wantCode: codes.InvalidArgument},
		{name: "empty batch", req: &filer_pb.LookupDirectoryEntriesRequest{}, wantCode: codes.InvalidArgument},
		{name: "oversized", req: repeatedBatchLookupRequest(maxFilerBatchLookupRequests+1, valid), wantCode: codes.ResourceExhausted},
		{name: "nil item", req: &filer_pb.LookupDirectoryEntriesRequest{Requests: []*filer_pb.LookupDirectoryEntryRequest{nil}}, wantCode: codes.InvalidArgument},
		{name: "relative directory", req: &filer_pb.LookupDirectoryEntriesRequest{Requests: []*filer_pb.LookupDirectoryEntryRequest{{Directory: "batch", Name: "key"}}}, wantCode: codes.InvalidArgument},
		{name: "empty name", req: &filer_pb.LookupDirectoryEntriesRequest{Requests: []*filer_pb.LookupDirectoryEntryRequest{{Directory: "/batch"}}}, wantCode: codes.InvalidArgument},
		{name: "nested name", req: &filer_pb.LookupDirectoryEntriesRequest{Requests: []*filer_pb.LookupDirectoryEntryRequest{{Directory: "/batch", Name: "a/b"}}}, wantCode: codes.InvalidArgument},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			called := false
			_, err := lookupDirectoryEntries(context.Background(), tt.req, 17,
				func(context.Context, util.FullPath) (*filer.Entry, int64, error) {
					called = true
					return nil, 0, nil
				},
				func(context.Context, []string) (map[string][]wdclient.Location, error) {
					called = true
					return nil, nil
				},
			)
			if status.Code(err) != tt.wantCode {
				t.Fatalf("error code = %v, want %v: %v", status.Code(err), tt.wantCode, err)
			}
			if called {
				t.Fatal("invalid request reached lookup implementation")
			}
		})
	}
}

func TestLookupDirectoryEntriesAcceptsMaximumBatch(t *testing.T) {
	req := repeatedBatchLookupRequest(maxFilerBatchLookupRequests, &filer_pb.LookupDirectoryEntryRequest{
		Directory: "/batch",
		Name:      "key",
	})
	response, err := lookupDirectoryEntries(context.Background(), req, 13,
		func(context.Context, util.FullPath) (*filer.Entry, int64, error) {
			return nil, 99, filer_pb.ErrNotFound
		},
		func(context.Context, []string) (map[string][]wdclient.Location, error) {
			t.Fatal("volume lookup called for an all-miss batch")
			return nil, nil
		},
	)
	if err != nil {
		t.Fatalf("maximum-sized batch failed: %v", err)
	}
	if got := len(response.Results); got != maxFilerBatchLookupRequests {
		t.Fatalf("result count = %d, want %d", got, maxFilerBatchLookupRequests)
	}
	for _, index := range []int{0, maxFilerBatchLookupRequests - 1} {
		if got := response.Results[index]; got.LogTsNs != 99 || got.LogSignature != 13 {
			t.Fatalf("miss fence[%d] = (%d,%d), want (99,13)", index, got.LogTsNs, got.LogSignature)
		}
	}
}

func TestLookupDirectoryEntriesUsesFilerSignatureForFoundAndMiss(t *testing.T) {
	store := newRenameTestStore()
	if err := store.InsertEntry(context.Background(), newFileEntry("/batch/found", 1)); err != nil {
		t.Fatalf("seed entry: %v", err)
	}
	testFiler := newRenameTestFiler(t, store)
	testFiler.Signature = 73
	server := &FilerServer{
		filer:          testFiler,
		entryLockTable: util.NewLockTable[util.FullPath](),
	}

	response, err := server.LookupDirectoryEntries(context.Background(), &filer_pb.LookupDirectoryEntriesRequest{
		Requests: []*filer_pb.LookupDirectoryEntryRequest{
			{Directory: "/batch", Name: "found"},
			{Directory: "/batch", Name: "missing"},
		},
	})
	if err != nil {
		t.Fatalf("batch lookup: %v", err)
	}
	if !response.Results[0].Found || response.Results[1].Found {
		t.Fatalf("unexpected found states: %v, %v", response.Results[0].Found, response.Results[1].Found)
	}
	for index, result := range response.Results {
		if result.LogSignature != 73 || result.LogTsNs == 0 {
			t.Fatalf("result[%d] fence = (%d,%d), want nonzero timestamp and signature 73", index, result.LogTsNs, result.LogSignature)
		}
	}
}

func TestPopulateBatchReadAuthMintsExactFidCapabilities(t *testing.T) {
	response := &filer_pb.LookupDirectoryEntriesResponse{Results: []*filer_pb.LookupDirectoryEntryResult{
		{Found: true, Entry: &filer_pb.Entry{Chunks: []*filer_pb.FileChunk{
			{FileId: "7,abc"}, {FileId: "8,def"}, {FileId: "7,abc"},
		}}},
		{Found: false},
	}}
	populateBatchReadAuth(response, func(fid string) string { return "jwt:" + fid })
	if !reflect.DeepEqual(response.ReadAuth, map[string]string{
		"7,abc": "jwt:7,abc",
		"8,def": "jwt:8,def",
	}) {
		t.Fatalf("read auth = %#v", response.ReadAuth)
	}
}

func TestPopulateBatchReadAuthDoesNotInventCapabilitiesWhenSigningIsDisabled(t *testing.T) {
	response := &filer_pb.LookupDirectoryEntriesResponse{Results: []*filer_pb.LookupDirectoryEntryResult{
		{Found: true, Entry: &filer_pb.Entry{Chunks: []*filer_pb.FileChunk{{FileId: "7,abc"}}}},
	}}
	populateBatchReadAuth(response, func(string) string { return "" })
	if len(response.ReadAuth) != 0 {
		t.Fatalf("read auth must be empty without a signing key: %#v", response.ReadAuth)
	}
}

func TestLookupDirectoryEntriesPreservesOrderBoundsParallelismAndDeduplicatesVolumes(t *testing.T) {
	const requestCount = 96
	requests := make([]*filer_pb.LookupDirectoryEntryRequest, requestCount)
	for index := range requests {
		requests[index] = &filer_pb.LookupDirectoryEntryRequest{
			Directory: "/batch",
			Name:      fmt.Sprintf("item-%03d", index),
		}
	}

	var active atomic.Int32
	var activeMax atomic.Int32
	findEntry := func(_ context.Context, path util.FullPath) (*filer.Entry, int64, error) {
		index, err := strconv.Atoi(strings.TrimPrefix(string(path), "/batch/item-"))
		if err != nil {
			return nil, 0, err
		}
		current := active.Add(1)
		defer active.Add(-1)
		for {
			previous := activeMax.Load()
			if current <= previous || activeMax.CompareAndSwap(previous, current) {
				break
			}
		}
		// Reverse completion order within each wave so append-based implementations fail.
		time.Sleep(time.Duration(requestCount-index) * 50 * time.Microsecond)
		if index == 7 {
			return nil, int64(1000 + index), errors.New("lookup failed")
		}
		if index == 11 {
			return nil, int64(1000 + index), filer_pb.ErrNotFound
		}
		volumeID := uint32(index%3 + 1)
		chunk := &filer_pb.FileChunk{Fid: &filer_pb.FileId{VolumeId: volumeID, FileKey: uint64(index + 1), Cookie: 17}}
		if index == 5 {
			chunk = &filer_pb.FileChunk{FileId: "2,0294cbb9892b"}
		}
		return &filer.Entry{FullPath: path, Chunks: []*filer_pb.FileChunk{chunk, chunk}}, int64(1000 + index), nil
	}

	volumeLookupCalls := 0
	var lookedUpVolumeIDs []string
	lookupVolumes := func(_ context.Context, volumeIDs []string) (map[string][]wdclient.Location, error) {
		volumeLookupCalls++
		lookedUpVolumeIDs = append([]string(nil), volumeIDs...)
		return map[string][]wdclient.Location{
			"1": {{Url: "volume-1:8080"}},
			"2": {{Url: "volume-2:8080"}},
			"3": {{Url: "volume-3:8080"}},
		}, nil
	}

	const logSignature = 0x1234
	response, err := lookupDirectoryEntries(context.Background(), &filer_pb.LookupDirectoryEntriesRequest{Requests: requests}, logSignature, findEntry, lookupVolumes)
	if err != nil {
		t.Fatalf("batch lookup: %v", err)
	}
	if got := activeMax.Load(); got <= 1 || got > maxFilerBatchLookupWorkers {
		t.Fatalf("parallel workers peaked at %d, want 2..%d", got, maxFilerBatchLookupWorkers)
	}
	if volumeLookupCalls != 1 {
		t.Fatalf("volume lookup calls = %d, want 1", volumeLookupCalls)
	}
	if want := []string{"1", "2", "3"}; !reflect.DeepEqual(lookedUpVolumeIDs, want) {
		t.Fatalf("volume IDs = %v, want %v", lookedUpVolumeIDs, want)
	}
	if got := len(response.LocationsMap); got != 3 {
		t.Fatalf("location map size = %d, want 3", got)
	}

	for index, result := range response.Results {
		if result.LogTsNs != int64(1000+index) || result.LogSignature != logSignature {
			t.Fatalf("result[%d] fence = (%d,%d), want (%d,%d)", index, result.LogTsNs, result.LogSignature, 1000+index, logSignature)
		}
		switch index {
		case 7:
			if result.Found || result.Entry != nil || result.Error != "lookup failed" {
				t.Fatalf("error result[%d] = %+v", index, result)
			}
		case 11:
			if result.Found || result.Entry != nil || result.Error != "" {
				t.Fatalf("not-found result[%d] = %+v", index, result)
			}
		default:
			if !result.Found || result.Entry == nil || result.Error != "" {
				t.Fatalf("found result[%d] = %+v", index, result)
			}
			if want := fmt.Sprintf("item-%03d", index); result.Entry.Name != want {
				t.Fatalf("result[%d] name = %q, want %q", index, result.Entry.Name, want)
			}
		}
	}
}

func TestLookupDirectoryEntriesMarksEntriesWithUnavailableVolumes(t *testing.T) {
	response, err := lookupDirectoryEntries(context.Background(), &filer_pb.LookupDirectoryEntriesRequest{
		Requests: []*filer_pb.LookupDirectoryEntryRequest{{Directory: "/batch", Name: "key"}},
	}, 42, func(_ context.Context, path util.FullPath) (*filer.Entry, int64, error) {
		return &filer.Entry{
			FullPath: path,
			Chunks: []*filer_pb.FileChunk{{
				Fid: &filer_pb.FileId{VolumeId: 9, FileKey: 1, Cookie: 2},
			}},
		}, 123, nil
	}, func(_ context.Context, volumeIDs []string) (map[string][]wdclient.Location, error) {
		if !sort.StringsAreSorted(volumeIDs) {
			t.Fatalf("volume IDs are not sorted: %v", volumeIDs)
		}
		return nil, errors.New("master unavailable")
	})
	if err != nil {
		t.Fatalf("batch lookup: %v", err)
	}
	result := response.Results[0]
	if result.LogTsNs != 123 || result.LogSignature != 42 {
		t.Fatalf("result fence = (%d,%d), want (123,42)", result.LogTsNs, result.LogSignature)
	}
	if !result.Found || result.Entry == nil {
		t.Fatalf("metadata result lost when volume lookup failed: %+v", result)
	}
	if !strings.Contains(result.Error, "volume 9 has no locations") || !strings.Contains(result.Error, "master unavailable") {
		t.Fatalf("result error = %q", result.Error)
	}
	locations, ok := response.LocationsMap["9"]
	if !ok || locations == nil || len(locations.Locations) != 0 {
		t.Fatalf("missing volume map entry = %#v, present=%v", locations, ok)
	}
}

func TestLookupDirectoryEntriesKeepsMalformedEntryError(t *testing.T) {
	response, err := lookupDirectoryEntries(context.Background(), &filer_pb.LookupDirectoryEntriesRequest{
		Requests: []*filer_pb.LookupDirectoryEntryRequest{{Directory: "/batch", Name: "key"}},
	}, 42, func(_ context.Context, path util.FullPath) (*filer.Entry, int64, error) {
		return &filer.Entry{
			FullPath: path,
			Chunks: []*filer_pb.FileChunk{
				{Fid: &filer_pb.FileId{VolumeId: 9, FileKey: 1, Cookie: 2}},
				{FileId: "not-a-file-id"},
			},
		}, 123, nil
	}, func(context.Context, []string) (map[string][]wdclient.Location, error) {
		return map[string][]wdclient.Location{}, nil
	})
	if err != nil {
		t.Fatalf("batch lookup: %v", err)
	}
	result := response.Results[0]
	if !result.Found || result.Entry == nil || !strings.Contains(result.Error, "invalid file id") {
		t.Fatalf("malformed entry was reported as a clean miss: %+v", result)
	}
}

func cacheVolumeEntry(_ context.Context, path util.FullPath) (*filer.Entry, int64, error) {
	return &filer.Entry{
		FullPath: path,
		Chunks: []*filer_pb.FileChunk{{
			Fid: &filer_pb.FileId{VolumeId: 9, FileKey: 1, Cookie: 2},
		}},
	}, 123, nil
}

func TestLookupDirectoryEntriesCanTreatRetiredCacheVolumeAsMiss(t *testing.T) {
	response, err := lookupDirectoryEntries(context.Background(), &filer_pb.LookupDirectoryEntriesRequest{
		Requests:                []*filer_pb.LookupDirectoryEntryRequest{{Directory: "/batch", Name: "key"}},
		UnavailableVolumeIsMiss: true,
	}, 42, cacheVolumeEntry, func(_ context.Context, volumeIDs []string) (map[string][]wdclient.Location, error) {
		// The master answered and left volume 9 out.
		return map[string][]wdclient.Location{}, errors.New("volume 9: volume id 9 not found")
	})
	if err != nil {
		t.Fatalf("batch lookup: %v", err)
	}
	result := response.Results[0]
	if result.Found || result.Entry != nil || result.Error != "" {
		t.Fatalf("retired cache volume did not become a clean miss: %+v", result)
	}
	if result.LogTsNs != 123 || result.LogSignature != 42 {
		t.Fatalf("miss fence = (%d,%d), want (123,42)", result.LogTsNs, result.LogSignature)
	}
	if _, exists := response.LocationsMap["9"]; exists {
		t.Fatalf("cache miss returned an unusable location: %#v", response.LocationsMap["9"])
	}
}

func TestLookupDirectoryEntriesKeepsCacheEntryWhenMasterIsUnreachable(t *testing.T) {
	response, err := lookupDirectoryEntries(context.Background(), &filer_pb.LookupDirectoryEntriesRequest{
		Requests:                []*filer_pb.LookupDirectoryEntryRequest{{Directory: "/batch", Name: "key"}},
		UnavailableVolumeIsMiss: true,
	}, 42, cacheVolumeEntry, func(context.Context, []string) (map[string][]wdclient.Location, error) {
		return nil, errors.New("master unavailable")
	})
	if err != nil {
		t.Fatalf("batch lookup: %v", err)
	}
	result := response.Results[0]
	if !result.Found || result.Entry == nil {
		t.Fatalf("unanswered lookup was reported as a miss: %+v", result)
	}
	if !strings.Contains(result.Error, "volume 9 has no locations") || !strings.Contains(result.Error, "master unavailable") {
		t.Fatalf("result error = %q", result.Error)
	}
	locations, ok := response.LocationsMap["9"]
	if !ok || locations == nil || len(locations.Locations) != 0 {
		t.Fatalf("unresolved volume map entry = %#v, present=%v", locations, ok)
	}
}

func TestLookupDirectoryEntriesKeepsMalformedEntryErrorInCacheMode(t *testing.T) {
	response, err := lookupDirectoryEntries(context.Background(), &filer_pb.LookupDirectoryEntriesRequest{
		Requests:                []*filer_pb.LookupDirectoryEntryRequest{{Directory: "/batch", Name: "key"}},
		UnavailableVolumeIsMiss: true,
	}, 42, func(_ context.Context, path util.FullPath) (*filer.Entry, int64, error) {
		return &filer.Entry{
			FullPath: path,
			Chunks: []*filer_pb.FileChunk{
				{Fid: &filer_pb.FileId{VolumeId: 9, FileKey: 1, Cookie: 2}},
				{FileId: "not-a-file-id"},
			},
		}, 123, nil
	}, func(context.Context, []string) (map[string][]wdclient.Location, error) {
		return map[string][]wdclient.Location{}, nil
	})
	if err != nil {
		t.Fatalf("batch lookup: %v", err)
	}
	result := response.Results[0]
	if !result.Found || result.Entry == nil || !strings.Contains(result.Error, "invalid file id") {
		t.Fatalf("malformed entry was reported as a clean miss: %+v", result)
	}
}

func TestCacheBatchLookupSelectsAuthoritativeVolumeLocations(t *testing.T) {
	cachedCalls, authoritativeCalls := 0, 0
	cached := func(context.Context, []string) (map[string][]wdclient.Location, error) {
		cachedCalls++
		return map[string][]wdclient.Location{"9": {{Url: "retired-volume:8080"}}}, nil
	}
	authoritative := func(context.Context, []string) (map[string][]wdclient.Location, error) {
		authoritativeCalls++
		return nil, nil
	}

	lookup := selectFilerBatchVolumeLookup(&filer_pb.LookupDirectoryEntriesRequest{
		UnavailableVolumeIsMiss: true,
	}, cached, authoritative)
	locations, err := lookup(context.Background(), []string{"9"})
	if err != nil {
		t.Fatal(err)
	}
	if cachedCalls != 0 || authoritativeCalls != 1 || len(locations) != 0 {
		t.Fatalf("cache lookup used stale path: cached=%d authoritative=%d locations=%v", cachedCalls, authoritativeCalls, locations)
	}

	lookup = selectFilerBatchVolumeLookup(&filer_pb.LookupDirectoryEntriesRequest{}, cached, authoritative)
	if _, err := lookup(context.Background(), []string{"9"}); err != nil {
		t.Fatal(err)
	}
	if cachedCalls != 1 || authoritativeCalls != 1 {
		t.Fatalf("ordinary lookup did not keep cached path: cached=%d authoritative=%d", cachedCalls, authoritativeCalls)
	}
}

func repeatedBatchLookupRequest(count int, request *filer_pb.LookupDirectoryEntryRequest) *filer_pb.LookupDirectoryEntriesRequest {
	requests := make([]*filer_pb.LookupDirectoryEntryRequest, count)
	for index := range requests {
		requests[index] = &filer_pb.LookupDirectoryEntryRequest{
			Directory: request.Directory,
			Name:      request.Name,
		}
	}
	return &filer_pb.LookupDirectoryEntriesRequest{Requests: requests}
}
