//go:build integration

package store

import (
	"context"
	"errors"
	"testing"
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"

	"bountyboard/internal/testutil"
	"bountyboard/internal/ulid"
)

func TestEnsureIndexesIsIdempotent(t *testing.T) {
	db := testutil.MongoDB(t)
	ctx := context.Background()

	if err := EnsureIndexes(ctx, db); err != nil {
		t.Fatalf("first run: %v", err)
	}
	if err := EnsureIndexes(ctx, db); err != nil {
		t.Fatalf("second run must be a no-op: %v", err)
	}

	// Spot-check a few critical indexes actually exist.
	checks := map[string]string{
		"users":    "email_1",
		"tasks":    "uniq_external_ref",
		"sessions": "expiresAt_1",
	}
	for coll, wantName := range checks {
		cur, err := db.Collection(coll).Indexes().List(ctx)
		if err != nil {
			t.Fatal(err)
		}
		var specs []bson.M
		if err := cur.All(ctx, &specs); err != nil {
			t.Fatal(err)
		}
		found := false
		for _, s := range specs {
			if s["name"] == wantName {
				found = true
				// the upsert key must be unique and partial (external-only)
				if coll == "tasks" {
					if s["unique"] != true || s["partialFilterExpression"] == nil {
						t.Errorf("%s/%s: want unique partial, got %v", coll, wantName, s)
					}
				}
			}
		}
		if !found {
			t.Errorf("%s: index %s not found in %v", coll, wantName, specs)
		}
	}
}

func TestUpdateVersioned(t *testing.T) {
	db := testutil.MongoDB(t)
	ctx := context.Background()
	coll := db.Collection("tasks")

	id := ulid.New()
	if err := InsertOne(ctx, coll, id, bson.M{"title": "original", "status": "imported"}); err != nil {
		t.Fatal(err)
	}

	// Happy path bumps version and updatedAt.
	if err := UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"title": "edited"}}); err != nil {
		t.Fatalf("update at correct version: %v", err)
	}
	var doc struct {
		Title     string    `bson:"title"`
		Version   int64     `bson:"version"`
		UpdatedAt time.Time `bson:"updatedAt"`
		CreatedAt time.Time `bson:"createdAt"`
	}
	if err := coll.FindOne(ctx, bson.M{"_id": id}).Decode(&doc); err != nil {
		t.Fatal(err)
	}
	if doc.Title != "edited" || doc.Version != 2 {
		t.Fatalf("after update: title=%q version=%d", doc.Title, doc.Version)
	}
	if !doc.UpdatedAt.After(doc.CreatedAt) {
		t.Error("updatedAt must advance")
	}

	// Stale version loses.
	err := UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"title": "stale write"}})
	if !errors.Is(err, ErrVersionConflict) {
		t.Fatalf("want ErrVersionConflict, got %v", err)
	}

	// Missing document is distinguished from a conflict.
	err = UpdateVersioned(ctx, coll, ulid.New(), 1, bson.M{"$set": bson.M{"title": "x"}})
	if !errors.Is(err, ErrNotFound) {
		t.Fatalf("want ErrNotFound, got %v", err)
	}

	// Non-$set operators pass through; version still bumps.
	if err := UpdateVersioned(ctx, coll, id, 2, bson.M{
		"$push": bson.M{"timeline": bson.M{"event": "published"}},
	}); err != nil {
		t.Fatalf("push update: %v", err)
	}
	var doc2 struct {
		Version  int64    `bson:"version"`
		Timeline []bson.M `bson:"timeline"`
	}
	if err := coll.FindOne(ctx, bson.M{"_id": id}).Decode(&doc2); err != nil {
		t.Fatal(err)
	}
	if doc2.Version != 3 || len(doc2.Timeline) != 1 {
		t.Fatalf("after push: version=%d timeline=%d", doc2.Version, len(doc2.Timeline))
	}

	// Touching version inside the update is a programming error.
	if err := UpdateVersioned(ctx, coll, id, 3, bson.M{"$inc": bson.M{"version": 5}}); err == nil {
		t.Fatal("must reject updates that touch version")
	}
}

func TestUpdateVersionedConcurrentWriters(t *testing.T) {
	db := testutil.MongoDB(t)
	ctx := context.Background()
	coll := db.Collection("tasks")

	id := ulid.New()
	if err := InsertOne(ctx, coll, id, bson.M{"n": 0}); err != nil {
		t.Fatal(err)
	}

	// Two writers race at the same version: exactly one wins.
	results := make(chan error, 2)
	for i := 0; i < 2; i++ {
		go func(i int) {
			results <- UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"n": i + 1}})
		}(i)
	}
	var conflicts, oks int
	for i := 0; i < 2; i++ {
		switch err := <-results; {
		case err == nil:
			oks++
		case errors.Is(err, ErrVersionConflict):
			conflicts++
		default:
			t.Fatalf("unexpected error: %v", err)
		}
	}
	if oks != 1 || conflicts != 1 {
		t.Fatalf("oks=%d conflicts=%d, want exactly one of each", oks, conflicts)
	}
}
