package weed_server

import (
	"context"
	"testing"

	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)

// RECOMPUTE_LATEST must emit a metadata notification for the pointer it writes.
// Without it the recomputed latest-version pointer lives only in the filer that
// processed the mutation, so peer filers never learn the current version and
// their ListObjects undercount versioned buckets.
func TestRecomputeLatestEmitsPointerUpdateEvent(t *testing.T) {
	store := newRenameTestStore()
	store.entries["/buckets/b/obj.versions"] = newDirectoryEntry("/buckets/b/obj.versions", 10)
	version := newFileEntry("/buckets/b/obj.versions/v_123", 11)
	version.Extended = map[string][]byte{"vid": []byte("123")}
	store.entries["/buckets/b/obj.versions/v_123"] = version

	queue := &captureQueue{}
	swapNotificationQueue(t, queue)

	server := &FilerServer{filer: newRenameTestFiler(t, store)}
	m := &filer_pb.ObjectMutation{
		Type:      filer_pb.ObjectMutation_RECOMPUTE_LATEST,
		Directory: "/buckets/b",
		Name:      "obj.versions",
		Recompute: &filer_pb.Recompute{
			ScanDir:      "/buckets/b/obj.versions",
			Descending:   true,
			NameToKey:    "latest-file",
			CopyExtended: map[string]string{"latest-vid": "vid"},
		},
	}
	if err := server.applyObjectMutation(context.Background(), m, false, nil); err != nil {
		t.Fatalf("applyObjectMutation: %v", err)
	}

	// Pointer is persisted locally.
	ptr, err := store.FindEntry(context.Background(), "/buckets/b/obj.versions")
	if err != nil {
		t.Fatalf("find pointer: %v", err)
	}
	if got := string(ptr.Extended["latest-vid"]); got != "123" {
		t.Fatalf("pointer latest-vid = %q, want 123", got)
	}

	// ...and it is announced so peers replicate it.
	events := queue.snapshot()
	if len(events) != 1 {
		t.Fatalf("event count = %d, want 1", len(events))
	}
	e := events[0]
	if e.notification.NewEntry == nil || e.notification.NewEntry.Name != "obj.versions" {
		t.Fatalf("event new entry = %+v, want obj.versions", e.notification.NewEntry)
	}
	if got := string(e.notification.NewEntry.Extended["latest-vid"]); got != "123" {
		t.Fatalf("event latest-vid = %q, want 123", got)
	}
	if e.notification.OldEntry != nil && len(e.notification.OldEntry.Extended["latest-vid"]) != 0 {
		t.Fatalf("event old entry already had pointer: %+v", e.notification.OldEntry)
	}
}

// A pointer flip that demotes the prior latest must announce both the pointer
// update and the demote stamp, so a peer's view of both entries stays correct.
func TestRecomputeLatestDemoteEmitsEvent(t *testing.T) {
	store := newRenameTestStore()
	pointer := newDirectoryEntry("/buckets/b/obj.versions", 10)
	pointer.Extended = map[string][]byte{"latest-file": []byte("v_100"), "latest-vid": []byte("100")}
	store.entries["/buckets/b/obj.versions"] = pointer

	prior := newFileEntry("/buckets/b/obj.versions/v_100", 11)
	prior.Extended = map[string][]byte{"vid": []byte("100")}
	store.entries["/buckets/b/obj.versions/v_100"] = prior

	latest := newFileEntry("/buckets/b/obj.versions/v_200", 12)
	latest.Extended = map[string][]byte{"vid": []byte("200")}
	store.entries["/buckets/b/obj.versions/v_200"] = latest

	queue := &captureQueue{}
	swapNotificationQueue(t, queue)

	server := &FilerServer{filer: newRenameTestFiler(t, store)}
	m := &filer_pb.ObjectMutation{
		Type:      filer_pb.ObjectMutation_RECOMPUTE_LATEST,
		Directory: "/buckets/b",
		Name:      "obj.versions",
		Recompute: &filer_pb.Recompute{
			ScanDir:      "/buckets/b/obj.versions",
			Descending:   true,
			NameToKey:    "latest-file",
			CopyExtended: map[string]string{"latest-vid": "vid"},
			DemoteKey:    "noncurrent-since",
			DemoteValue:  []byte("999"),
		},
	}
	if err := server.applyObjectMutation(context.Background(), m, false, nil); err != nil {
		t.Fatalf("applyObjectMutation: %v", err)
	}

	gotPtr, err := store.FindEntry(context.Background(), "/buckets/b/obj.versions")
	if err != nil {
		t.Fatalf("find pointer: %v", err)
	}
	if got := string(gotPtr.Extended["latest-file"]); got != "v_200" {
		t.Fatalf("pointer latest-file = %q, want v_200", got)
	}
	gotPrior, err := store.FindEntry(context.Background(), "/buckets/b/obj.versions/v_100")
	if err != nil {
		t.Fatalf("find prior: %v", err)
	}
	if got := string(gotPrior.Extended["noncurrent-since"]); got != "999" {
		t.Fatalf("prior noncurrent-since = %q, want 999", got)
	}

	var sawPointer, sawDemote bool
	for _, e := range queue.snapshot() {
		if e.notification.NewEntry == nil {
			continue
		}
		switch e.notification.NewEntry.Name {
		case "obj.versions":
			if string(e.notification.NewEntry.Extended["latest-file"]) == "v_200" {
				sawPointer = true
			}
		case "v_100":
			if string(e.notification.NewEntry.Extended["noncurrent-since"]) == "999" {
				sawDemote = true
			}
		}
	}
	if !sawPointer {
		t.Fatalf("missing pointer-flip event: %+v", queue.snapshot())
	}
	if !sawDemote {
		t.Fatalf("missing demote event: %+v", queue.snapshot())
	}
}
