package user_test

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

	"galaxy/backend/internal/user"

	"github.com/google/uuid"
)

// TestSoftDeleteCascadeRunsAuthLobbyNotificationGeoInOrder verifies the
// documented cascade order. The test uses recording stubs for every
// hook and asserts that each one received the soft-delete signal
// exactly once for the right user_id.
func TestSoftDeleteCascadeRunsAuthLobbyNotificationGeoInOrder(t *testing.T) {
	db := startPostgres(t)

	revoker := &orderTracker{name: "auth"}
	lobby := &orderingLobbyCascade{name: "lobby"}
	notif := &orderingNotificationCascade{name: "notification"}
	geo := &orderingGeoCascade{name: "geo"}

	var order []string
	revoker.appendTo = func(s string) { order = append(order, s) }
	lobby.appendTo = func(s string) { order = append(order, s) }
	notif.appendTo = func(s string) { order = append(order, s) }
	geo.appendTo = func(s string) { order = append(order, s) }

	svc := user.NewService(user.Deps{

		Store:              user.NewStore(db),
		Cache:              user.NewCache(),
		Lobby:              lobby,
		Notification:       notif,
		Geo:                geo,
		SessionRevoker:     revoker,
		UserNameMaxRetries: 10,
		Now:                time.Now,
	})

	uid, err := svc.EnsureByEmail(context.Background(), "leo@example.test", "en", "UTC", "")
	if err != nil {
		t.Fatalf("EnsureByEmail: %v", err)
	}
	if err := svc.SoftDelete(context.Background(), uid, user.ActorRef{Type: "user", ID: uid.String()}); err != nil {
		t.Fatalf("SoftDelete: %v", err)
	}
	want := []string{"auth", "lobby", "notification", "geo"}
	if !equalStrings(order, want) {
		t.Fatalf("cascade order = %v, want %v", order, want)
	}

	// Second call is a no-op — cascade must not fire again.
	if err := svc.SoftDelete(context.Background(), uid, user.ActorRef{Type: "user", ID: uid.String()}); err != nil {
		t.Fatalf("idempotent SoftDelete: %v", err)
	}
	if !equalStrings(order, want) {
		t.Fatalf("idempotent SoftDelete fired cascade again: %v", order)
	}
}

// TestSoftDeleteCascadeErrorDoesNotRollback covers the contract that
// cascade failures are surfaced to the caller but do not undo the
// `accounts.deleted_at` write.
func TestSoftDeleteCascadeErrorDoesNotRollback(t *testing.T) {
	db := startPostgres(t)
	failingNotif := &failingNotificationCascade{err: errors.New("notification down")}

	svc := user.NewService(user.Deps{

		Store:              user.NewStore(db),
		Cache:              user.NewCache(),
		Lobby:              &orderingLobbyCascade{},
		Notification:       failingNotif,
		Geo:                &orderingGeoCascade{},
		SessionRevoker:     &orderTracker{},
		UserNameMaxRetries: 10,
		Now:                time.Now,
	})

	uid, err := svc.EnsureByEmail(context.Background(), "mia@example.test", "en", "UTC", "")
	if err != nil {
		t.Fatalf("EnsureByEmail: %v", err)
	}
	err = svc.SoftDelete(context.Background(), uid, user.ActorRef{Type: "user", ID: uid.String()})
	if err == nil {
		t.Fatalf("SoftDelete returned nil despite failing cascade")
	}
	if !errors.Is(err, failingNotif.err) {
		t.Fatalf("SoftDelete error = %v, want join containing %v", err, failingNotif.err)
	}

	var deletedAt *time.Time
	if scanErr := db.QueryRowContext(context.Background(),
		`SELECT deleted_at FROM backend.accounts WHERE user_id = $1`, uid,
	).Scan(&deletedAt); scanErr != nil {
		t.Fatalf("SELECT deleted_at: %v", scanErr)
	}
	if deletedAt == nil {
		t.Fatalf("deleted_at = NULL despite SoftDelete commit")
	}
}

func equalStrings(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// orderTracker spies on a single call kind and pushes its name into
// the ordered slice when invoked. It satisfies user.SessionRevoker.
type orderTracker struct {
	name      string
	calls     int
	lastUser  uuid.UUID
	lastActor user.SessionRevokeActor
	appendTo  func(string)
}

func (r *orderTracker) RevokeAllForUser(_ context.Context, userID uuid.UUID, actor user.SessionRevokeActor) error {
	r.calls++
	r.lastUser = userID
	r.lastActor = actor
	if r.appendTo != nil && r.name != "" {
		r.appendTo(r.name)
	}
	return nil
}

type orderingLobbyCascade struct {
	name     string
	appendTo func(string)
	deleted  int
	blocked  int
}

func (c *orderingLobbyCascade) OnUserDeleted(_ context.Context, _ uuid.UUID) error {
	c.deleted++
	if c.appendTo != nil && c.name != "" {
		c.appendTo(c.name)
	}
	return nil
}

func (c *orderingLobbyCascade) OnUserBlocked(_ context.Context, _ uuid.UUID) error {
	c.blocked++
	return nil
}

type orderingNotificationCascade struct {
	name     string
	appendTo func(string)
	calls    int
}

func (c *orderingNotificationCascade) OnUserDeleted(_ context.Context, _ uuid.UUID) error {
	c.calls++
	if c.appendTo != nil && c.name != "" {
		c.appendTo(c.name)
	}
	return nil
}

type orderingGeoCascade struct {
	name     string
	appendTo func(string)
	calls    int
}

func (c *orderingGeoCascade) OnUserDeleted(_ context.Context, _ uuid.UUID) error {
	c.calls++
	if c.appendTo != nil && c.name != "" {
		c.appendTo(c.name)
	}
	return nil
}

type failingNotificationCascade struct {
	err   error
	calls int
}

func (c *failingNotificationCascade) OnUserDeleted(_ context.Context, _ uuid.UUID) error {
	c.calls++
	return c.err
}
