package iceberg

import (
	"context"
	"errors"
	"path"
	"strings"
	"testing"

	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"google.golang.org/grpc"
)

// stubFilerClient answers only the calls the metadata writers make, tracking
// which paths exist so exclusive creates can be exercised.
type stubFilerClient struct {
	filer_pb.SeaweedFilerClient
	entries map[string][]byte
}

func newStubFilerClient() *stubFilerClient {
	return &stubFilerClient{entries: make(map[string][]byte)}
}

func (c *stubFilerClient) WithFilerClient(_ bool, fn func(client filer_pb.SeaweedFilerClient) error) error {
	return fn(c)
}

func (c *stubFilerClient) LookupDirectoryEntry(_ context.Context, req *filer_pb.LookupDirectoryEntryRequest, _ ...grpc.CallOption) (*filer_pb.LookupDirectoryEntryResponse, error) {
	key := path.Join(req.Directory, req.Name)
	if _, ok := c.entries[key]; !ok {
		return nil, filer_pb.ErrNotFound
	}
	return &filer_pb.LookupDirectoryEntryResponse{Entry: &filer_pb.Entry{Name: req.Name}}, nil
}

func (c *stubFilerClient) CreateEntry(_ context.Context, req *filer_pb.CreateEntryRequest, _ ...grpc.CallOption) (*filer_pb.CreateEntryResponse, error) {
	key := path.Join(req.Directory, req.Entry.Name)
	if _, exists := c.entries[key]; exists && req.OExcl {
		return &filer_pb.CreateEntryResponse{
			Error:     "entry already exists",
			ErrorCode: filer_pb.FilerError_ENTRY_ALREADY_EXISTS,
		}, nil
	}
	c.entries[key] = req.Entry.Content
	return &filer_pb.CreateEntryResponse{}, nil
}

// Two commits racing off the same base metadata pick the same v{N} file name.
// The loser must be told, not allowed to replace the winner's metadata.
func TestSaveMetadataFileExclusiveRefusesToOverwrite(t *testing.T) {
	client := newStubFilerClient()
	s := &Server{filerClient: client}
	ctx := context.Background()

	if err := s.saveMetadataFile(ctx, "bkt", "ns/tbl", "v2.metadata.json", []byte(`{"winner":true}`), true); err != nil {
		t.Fatalf("first write failed: %v", err)
	}

	err := s.saveMetadataFile(ctx, "bkt", "ns/tbl", "v2.metadata.json", []byte(`{"loser":true}`), true)
	if !errors.Is(err, filer_pb.ErrEntryAlreadyExists) {
		t.Fatalf("second write err = %v, want ErrEntryAlreadyExists", err)
	}

	stored := string(client.entries[path.Join(metadataDirPath("bkt", "ns/tbl"), "v2.metadata.json")])
	if stored != `{"winner":true}` {
		t.Errorf("stored metadata = %s, want the first writer's content", stored)
	}
}

// A metadata file left behind by an interrupted commit must not wedge the
// table: the next commit stages under a unique name rather than failing or
// overwriting, and the catalog pointer still decides the winner.
func TestStageCommitMetadataFallsBackToAUniqueName(t *testing.T) {
	client := newStubFilerClient()
	s := &Server{filerClient: client}
	ctx := context.Background()

	name, location, err := s.stageCommitMetadata(ctx, "bkt", "ns/tbl", "s3://bkt/ns/tbl", "v2.metadata.json", []byte(`{"orphan":true}`))
	if err != nil {
		t.Fatalf("first stage failed: %v", err)
	}
	if name != "v2.metadata.json" {
		t.Errorf("first stage used %q, want the plain versioned name", name)
	}
	if location != "s3://bkt/ns/tbl/metadata/v2.metadata.json" {
		t.Errorf("location = %q", location)
	}

	name, location, err = s.stageCommitMetadata(ctx, "bkt", "ns/tbl", "s3://bkt/ns/tbl", "v2.metadata.json", []byte(`{"second":true}`))
	if err != nil {
		t.Fatalf("second stage failed: %v", err)
	}
	if !strings.HasPrefix(name, "v2-") || !strings.HasSuffix(name, ".metadata.json") {
		t.Errorf("second stage used %q, want a unique v2-* name", name)
	}
	if location != "s3://bkt/ns/tbl/metadata/"+name {
		t.Errorf("location = %q, want it to match the staged name", location)
	}

	stored := string(client.entries[path.Join(metadataDirPath("bkt", "ns/tbl"), "v2.metadata.json")])
	if stored != `{"orphan":true}` {
		t.Errorf("the first file was overwritten: %s", stored)
	}
	if fallback := string(client.entries[path.Join(metadataDirPath("bkt", "ns/tbl"), name)]); fallback != `{"second":true}` {
		t.Errorf("fallback file holds %s, want the second writer's metadata", fallback)
	}
}

// Paths that legitimately rewrite a file, such as manifest repair, keep the
// overwriting behaviour.
func TestSaveMetadataFileOverwrites(t *testing.T) {
	client := newStubFilerClient()
	s := &Server{filerClient: client}
	ctx := context.Background()

	if err := s.saveMetadataFile(ctx, "bkt", "ns/tbl", "v2.metadata.json", []byte(`{"first":true}`), false); err != nil {
		t.Fatalf("first write failed: %v", err)
	}
	if err := s.saveMetadataFile(ctx, "bkt", "ns/tbl", "v2.metadata.json", []byte(`{"second":true}`), false); err != nil {
		t.Fatalf("second write failed: %v", err)
	}

	stored := string(client.entries[path.Join(metadataDirPath("bkt", "ns/tbl"), "v2.metadata.json")])
	if stored != `{"second":true}` {
		t.Errorf("stored metadata = %s, want the second write", stored)
	}
}
