package filemanagerrealtime

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/shared/pkg/uuidv7"
)

func openStatusPollerTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	dsn := fmt.Sprintf("file:file_status_poller_%d?mode=memory&cache=shared", time.Now().UnixNano())
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
		NowFunc: func() time.Time { return time.Now().UTC() },
	})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}
	if err := db.AutoMigrate(&filemanager.FileStatusRealtimeEvent{}); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	if err := db.AutoMigrate(&filemanager.File{}); err != nil {
		t.Fatalf("auto migrate file: %v", err)
	}
	return db
}

func waitStatusEvent(t *testing.T, sub *Subscription) FileStatusEvent {
	t.Helper()
	select {
	case event := <-sub.Events():
		return event
	case <-time.After(time.Second):
		t.Fatalf("timed out waiting for status event")
	}
	return FileStatusEvent{}
}

func insertRealtimeEventForPollerTest(
	t *testing.T,
	db *gorm.DB,
	fileID []byte,
	userID []byte,
	status string,
	name string,
	reason string,
	occurredAt time.Time,
) {
	t.Helper()
	row := &filemanager.FileStatusRealtimeEvent{
		ID:            uuidv7.MustBytes(),
		FileID:        append([]byte(nil), fileID...),
		UserID:        append([]byte(nil), userID...),
		Status:        status,
		Name:          name,
		FailureReason: reason,
		OccurredAt:    occurredAt.UTC(),
		CreatedAt:     occurredAt.UTC(),
		UpdatedAt:     occurredAt.UTC(),
	}
	if err := db.WithContext(context.Background()).Create(row).Error; err != nil {
		t.Fatalf("insert realtime event: %v", err)
	}
}

func insertFileForPollerTest(t *testing.T, db *gorm.DB, fileID []byte, folderID []byte, mimeType string, name string) {
	t.Helper()
	now := time.Now().UTC()
	row := &filemanager.File{
		ID:             append([]byte(nil), fileID...),
		FolderID:       append([]byte(nil), folderID...),
		Name:           name,
		NameNormalized: strings.ToLower(strings.TrimSpace(name)),
		Extension:      "pdf",
		SizeBytes:      1,
		MimeType:       mimeType,
		Bucket:         "bucket",
		ObjectKey:      "obj-" + name,
		Status:         filemanager.FileStatusUploaded,
		NameSlot:       "live",
		CreatedAt:      now,
		UpdatedAt:      now,
	}
	if err := db.WithContext(context.Background()).Create(row).Error; err != nil {
		t.Fatalf("insert file: %v", err)
	}
}

func TestStatusPollerEmitsLifecycleEvents(t *testing.T) {
	db := openStatusPollerTestDB(t)
	fileID := uuidv7.MustBytes()
	fileUUID, err := uuidv7.BytesToString(fileID)
	if err != nil {
		t.Fatalf("file uuid: %v", err)
	}
	ownerID := uuidv7.MustBytes()
	ownerUUID, err := uuidv7.BytesToString(ownerID)
	if err != nil {
		t.Fatalf("owner uuid: %v", err)
	}
	folderID := uuidv7.MustBytes()
	folderUUID, err := uuidv7.BytesToString(folderID)
	if err != nil {
		t.Fatalf("folder uuid: %v", err)
	}
	insertFileForPollerTest(t, db, fileID, folderID, "application/pdf", "report.pdf")

	hub := NewHub(8, nil)
	sub, err := hub.Subscribe(ownerUUID)
	if err != nil {
		t.Fatalf("subscribe: %v", err)
	}
	defer sub.Close()

	poller := NewStatusPoller(db, hub, nil, StatusPollerConfig{
		PollInterval: time.Second,
		BatchSize:    50,
	})

	baseAt := time.Now().UTC().Add(20 * time.Millisecond)
	insertRealtimeEventForPollerTest(t, db, fileID, ownerID, filemanager.FileStatusUploaded, "report.pdf", "", baseAt)
	insertRealtimeEventForPollerTest(t, db, fileID, ownerID, filemanager.FileStatusProcessing, "report.pdf", "", baseAt.Add(time.Second))
	insertRealtimeEventForPollerTest(t, db, fileID, ownerID, filemanager.FileStatusValidated, "report.pdf", "", baseAt.Add(2*time.Second))
	insertRealtimeEventForPollerTest(t, db, fileID, ownerID, filemanager.FileStatusReady, "report.pdf", "", baseAt.Add(3*time.Second))

	processed, err := poller.PollOnce(context.Background())
	if err != nil {
		t.Fatalf("poll once: %v", err)
	}
	if processed != 4 {
		t.Fatalf("expected 4 published events, got %d", processed)
	}

	event := waitStatusEvent(t, sub)
	if event.Status != filemanager.FileStatusUploaded {
		t.Fatalf("expected uploaded status, got %s", event.Status)
	}
	if event.FileUUID != fileUUID {
		t.Fatalf("expected file uuid %s, got %s", fileUUID, event.FileUUID)
	}
	if event.FolderUUID == nil || !strings.EqualFold(*event.FolderUUID, folderUUID) {
		t.Fatalf("expected folder uuid %s, got %#v", folderUUID, event.FolderUUID)
	}
	if event.MimeType != "application/pdf" {
		t.Fatalf("expected mime type application/pdf, got %q", event.MimeType)
	}
	event = waitStatusEvent(t, sub)
	if event.Status != filemanager.FileStatusProcessing {
		t.Fatalf("expected processing status, got %s", event.Status)
	}
	event = waitStatusEvent(t, sub)
	if event.Status != filemanager.FileStatusValidated {
		t.Fatalf("expected validated status, got %s", event.Status)
	}
	event = waitStatusEvent(t, sub)
	if event.Status != filemanager.FileStatusReady {
		t.Fatalf("expected ready status, got %s", event.Status)
	}

	var deliveredCount int64
	if err := db.WithContext(context.Background()).
		Model(&filemanager.FileStatusRealtimeEvent{}).
		Where("delivered_at IS NOT NULL").
		Count(&deliveredCount).Error; err != nil {
		t.Fatalf("count delivered rows: %v", err)
	}
	if deliveredCount != 4 {
		t.Fatalf("expected 4 delivered rows, got %d", deliveredCount)
	}
}

func TestStatusPollerInvokesOnPublishedCallback(t *testing.T) {
	db := openStatusPollerTestDB(t)
	fileID := uuidv7.MustBytes()
	fileUUID, err := uuidv7.BytesToString(fileID)
	if err != nil {
		t.Fatalf("file uuid: %v", err)
	}
	userID := uuidv7.MustBytes()
	userUUID, err := uuidv7.BytesToString(userID)
	if err != nil {
		t.Fatalf("user uuid: %v", err)
	}
	folderID := uuidv7.MustBytes()
	insertFileForPollerTest(t, db, fileID, folderID, "application/pdf", "report.pdf")
	insertRealtimeEventForPollerTest(t, db, fileID, userID, filemanager.FileStatusReady, "report.pdf", "", time.Now().UTC().Add(20*time.Millisecond))

	hub := NewHub(4, nil)
	sub, err := hub.Subscribe(userUUID)
	if err != nil {
		t.Fatalf("subscribe: %v", err)
	}
	defer sub.Close()

	var callbackCount int
	var callbackEvent FileStatusEvent
	poller := NewStatusPoller(db, hub, nil, StatusPollerConfig{
		PollInterval: time.Second,
		BatchSize:    50,
		OnPublished: func(_ context.Context, event FileStatusEvent) error {
			callbackCount++
			callbackEvent = event
			return nil
		},
	})

	processed, err := poller.PollOnce(context.Background())
	if err != nil {
		t.Fatalf("poll once failed: %v", err)
	}
	if processed != 1 {
		t.Fatalf("expected one published event, got %d", processed)
	}
	if callbackCount != 1 {
		t.Fatalf("expected callback to run once, got %d", callbackCount)
	}
	if callbackEvent.Status != filemanager.FileStatusReady {
		t.Fatalf("expected ready callback event, got %s", callbackEvent.Status)
	}
	if callbackEvent.FileUUID != fileUUID {
		t.Fatalf("expected callback file uuid %s, got %s", fileUUID, callbackEvent.FileUUID)
	}
	_ = waitStatusEvent(t, sub)
}

func TestStatusPollerEmitsFailedReason(t *testing.T) {
	db := openStatusPollerTestDB(t)
	fileID := uuidv7.MustBytes()
	ownerID := uuidv7.MustBytes()
	ownerUUID, err := uuidv7.BytesToString(ownerID)
	if err != nil {
		t.Fatalf("owner uuid: %v", err)
	}

	hub := NewHub(4, nil)
	sub, err := hub.Subscribe(ownerUUID)
	if err != nil {
		t.Fatalf("subscribe: %v", err)
	}
	defer sub.Close()

	poller := NewStatusPoller(db, hub, nil, StatusPollerConfig{
		PollInterval: time.Second,
		BatchSize:    50,
	})

	insertRealtimeEventForPollerTest(
		t,
		db,
		fileID,
		ownerID,
		filemanager.FileStatusFailed,
		"invalid.pdf",
		"file bucket mismatch",
		time.Now().UTC().Add(20*time.Millisecond),
	)

	processed, err := poller.PollOnce(context.Background())
	if err != nil {
		t.Fatalf("poll once failed: %v", err)
	}
	if processed != 1 {
		t.Fatalf("expected one processed failed row, got %d", processed)
	}
	event := waitStatusEvent(t, sub)
	if event.Status != filemanager.FileStatusFailed {
		t.Fatalf("expected failed status event, got %s", event.Status)
	}
	if event.FailureReason == nil || *event.FailureReason == "" {
		t.Fatalf("expected failure reason on failed event")
	}
}

func TestStatusPollerSkipsRowsWithoutValidRecipient(t *testing.T) {
	db := openStatusPollerTestDB(t)
	fileID := uuidv7.MustBytes()
	invalidUserID := []byte{1, 2, 3}

	insertRealtimeEventForPollerTest(
		t,
		db,
		fileID,
		invalidUserID,
		filemanager.FileStatusUploaded,
		"nobody.pdf",
		"",
		time.Now().UTC().Add(20*time.Millisecond),
	)

	hub := NewHub(4, nil)
	poller := NewStatusPoller(db, hub, nil, StatusPollerConfig{
		PollInterval: time.Second,
		BatchSize:    50,
	})

	processed, err := poller.PollOnce(context.Background())
	if err != nil {
		t.Fatalf("poll once: %v", err)
	}
	if processed != 0 {
		t.Fatalf("expected zero emitted events for row without valid recipient, got %d", processed)
	}

	var deliveredCount int64
	if err := db.WithContext(context.Background()).
		Model(&filemanager.FileStatusRealtimeEvent{}).
		Where("delivered_at IS NOT NULL").
		Count(&deliveredCount).Error; err != nil {
		t.Fatalf("count delivered rows: %v", err)
	}
	if deliveredCount != 1 {
		t.Fatalf("expected skipped row to be marked delivered, got %d", deliveredCount)
	}
}
