package worker

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

	"wucher/internal/config"
	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/queue"
)

type stubFileOutboxRepo struct {
	messages map[string]*filemanager.FileProcessingOutboxMessage
}

func newStubFileOutboxRepo(messages ...*filemanager.FileProcessingOutboxMessage) *stubFileOutboxRepo {
	repo := &stubFileOutboxRepo{messages: map[string]*filemanager.FileProcessingOutboxMessage{}}
	for _, message := range messages {
		if message == nil {
			continue
		}
		cp := *message
		cp.ID = append([]byte(nil), message.ID...)
		cp.Body = append([]byte(nil), message.Body...)
		cp.Attributes = append([]byte(nil), message.Attributes...)
		repo.messages[string(cp.ID)] = &cp
	}
	return repo
}

func (r *stubFileOutboxRepo) CreateFileProcessingOutboxMessage(_ context.Context, message *filemanager.FileProcessingOutboxMessage) error {
	if message == nil {
		return errors.New("nil message")
	}
	cp := *message
	cp.ID = append([]byte(nil), message.ID...)
	cp.Body = append([]byte(nil), message.Body...)
	cp.Attributes = append([]byte(nil), message.Attributes...)
	r.messages[string(cp.ID)] = &cp
	return nil
}

func (r *stubFileOutboxRepo) ClaimPendingFileProcessingOutboxMessages(_ context.Context, limit int, now time.Time, lockTTL time.Duration, workerID string) ([]filemanager.FileProcessingOutboxMessage, error) {
	if limit <= 0 {
		limit = 1
	}
	if lockTTL <= 0 {
		lockTTL = time.Minute
	}
	out := make([]filemanager.FileProcessingOutboxMessage, 0, limit)
	staleBefore := now.Add(-lockTTL)
	for _, message := range r.messages {
		if len(out) >= limit {
			break
		}
		switch {
		case message.Status == filemanager.FileProcessingOutboxStatusPending && !message.AvailableAt.After(now):
		case message.Status == filemanager.FileProcessingOutboxStatusProcessing && message.LockedAt != nil && message.LockedAt.Before(staleBefore):
		default:
			continue
		}
		message.Status = filemanager.FileProcessingOutboxStatusProcessing
		message.Attempts++
		ts := now
		message.LockedAt = &ts
		message.LockedBy = workerID
		cp := *message
		out = append(out, cp)
	}
	return out, nil
}

func (r *stubFileOutboxRepo) MarkFileProcessingOutboxMessagePublished(_ context.Context, id []byte, publishedAt time.Time) error {
	message := r.messages[string(id)]
	if message == nil {
		return errors.New("not found")
	}
	message.Status = filemanager.FileProcessingOutboxStatusPublished
	message.PublishedAt = &publishedAt
	message.LockedAt = nil
	message.LockedBy = ""
	message.LastError = ""
	return nil
}

func (r *stubFileOutboxRepo) MarkFileProcessingOutboxMessageRetry(_ context.Context, id []byte, availableAt time.Time, lastErr string) error {
	message := r.messages[string(id)]
	if message == nil {
		return errors.New("not found")
	}
	message.Status = filemanager.FileProcessingOutboxStatusPending
	message.AvailableAt = availableAt
	message.LockedAt = nil
	message.LockedBy = ""
	message.LastError = lastErr
	return nil
}

func (r *stubFileOutboxRepo) MarkFileProcessingOutboxMessageDead(_ context.Context, id []byte, failedAt time.Time, lastErr string) error {
	message := r.messages[string(id)]
	if message == nil {
		return errors.New("not found")
	}
	message.Status = filemanager.FileProcessingOutboxStatusDead
	message.AvailableAt = failedAt
	message.LockedAt = nil
	message.LockedBy = ""
	message.LastError = lastErr
	return nil
}

func (r *stubFileOutboxRepo) CountPendingFileProcessingOutboxMessages(_ context.Context, now time.Time) (int64, error) {
	var count int64
	for _, message := range r.messages {
		if (message.Status == filemanager.FileProcessingOutboxStatusPending && !message.AvailableAt.After(now)) || message.Status == filemanager.FileProcessingOutboxStatusProcessing {
			count++
		}
	}
	return count, nil
}

func newFileOutboxRecord(t *testing.T, job queue.FileProcessingJob) *filemanager.FileProcessingOutboxMessage {
	t.Helper()
	serializer := queue.NewFileProcessingJSONSerializer("v1", queue.QueueTypeStandard, "")
	message, err := serializer.Serialize(context.Background(), job)
	if err != nil {
		t.Fatalf("serialize: %v", err)
	}
	record, err := filemanager.NewFileProcessingOutboxMessage(time.Now().UTC(), &filemanager.FileProcessingOutboundMessage{
		ID:              message.ID,
		Body:            append([]byte(nil), message.Body...),
		Attributes:      message.Attributes,
		MessageGroupID:  message.MessageGroupID,
		DeduplicationID: message.DeduplicationID,
	})
	if err != nil {
		t.Fatalf("new outbox message: %v", err)
	}
	return record
}

func TestFileProcessingOutboxDispatcherPublishSuccess(t *testing.T) {
	record := newFileOutboxRecord(t, queue.FileProcessingJob{
		FileUUID:  "01961f5c-6471-7b81-9df5-2e2e1cf7e498",
		Bucket:    "bucket-a",
		ObjectKey: "files/2026/04/05/object.pdf",
		SizeBytes: 128,
	})
	repo := newStubFileOutboxRepo(record)
	producer := &stubQueueProducer{}
	dispatcher := NewFileProcessingOutboxDispatcher(config.FileQueueConfig{
		OutboxEnabled:         true,
		OutboxBatchSize:       10,
		OutboxDispatchTimeout: time.Second,
		OutboxLockTTL:         time.Minute,
		OutboxMaxAttempts:     3,
		WorkerBackoffMin:      time.Second,
		WorkerBackoffMax:      time.Minute,
	}, repo, producer, nil)

	processed, err := dispatcher.dispatchOnce(context.Background())
	if err != nil {
		t.Fatalf("dispatch once: %v", err)
	}
	if processed != 1 {
		t.Fatalf("expected one processed message, got %d", processed)
	}
	if len(producer.published) != 1 {
		t.Fatalf("expected one published message, got %d", len(producer.published))
	}
	if repo.messages[string(record.ID)].Status != filemanager.FileProcessingOutboxStatusPublished {
		t.Fatalf("expected message published, got %s", repo.messages[string(record.ID)].Status)
	}
	if got := dispatcher.Metrics().Published; got != 1 {
		t.Fatalf("expected published metric=1, got %d", got)
	}
}

func TestFileProcessingOutboxDispatcherRetryOnPublishFailure(t *testing.T) {
	record := newFileOutboxRecord(t, queue.FileProcessingJob{
		FileUUID:  "01961f5c-6471-7b81-9df5-2e2e1cf7e499",
		Bucket:    "bucket-a",
		ObjectKey: "files/2026/04/05/object.pdf",
		SizeBytes: 256,
	})
	repo := newStubFileOutboxRepo(record)
	producer := &stubQueueProducer{err: errors.New("queue unavailable")}
	dispatcher := NewFileProcessingOutboxDispatcher(config.FileQueueConfig{
		OutboxEnabled:     true,
		OutboxBatchSize:   10,
		OutboxLockTTL:     time.Minute,
		OutboxMaxAttempts: 3,
		WorkerBackoffMin:  time.Second,
		WorkerBackoffMax:  time.Minute,
	}, repo, producer, nil)

	processed, err := dispatcher.dispatchOnce(context.Background())
	if err != nil {
		t.Fatalf("dispatch once: %v", err)
	}
	if processed != 1 {
		t.Fatalf("expected one processed message, got %d", processed)
	}
	got := repo.messages[string(record.ID)]
	if got.Status != filemanager.FileProcessingOutboxStatusPending {
		t.Fatalf("expected message returned to pending, got %s", got.Status)
	}
	if got.Attempts != 1 {
		t.Fatalf("expected attempts=1, got %d", got.Attempts)
	}
	if !got.AvailableAt.After(time.Now().Add(-time.Second)) {
		t.Fatalf("expected retry to schedule next attempt in the future")
	}
	if metrics := dispatcher.Metrics(); metrics.Retried != 1 {
		t.Fatalf("expected retried metric=1, got %d", metrics.Retried)
	}
}
