package outbox_test

import (
	"context"
	stderrors "errors"
	"sync"
	"testing"
	"time"

	"github.com/bytedance/sonic"
	"github.com/hibiken/asynq"
	"go.uber.org/zap"

	"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/internal/testutil"
	"github.com/break/junhong_cmp_fiber/pkg/constants"
	"github.com/break/junhong_cmp_fiber/pkg/queue"
)

type recordingPublisher struct {
	mu        sync.Mutex
	envelopes []outbox.DeliveryEnvelope
	err       error
}

func (p *recordingPublisher) Publish(_ context.Context, envelope outbox.DeliveryEnvelope) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.envelopes = append(p.envelopes, envelope)
	return p.err
}

func TestExpiredLeaseRedeliversOriginalEnvelope(t *testing.T) {
	db := testutil.NewPostgresTransaction(t)
	createTemporaryOutboxTables(t, db)
	repository := outbox.NewRepository()
	event, err := repository.Append(context.Background(), db, newEnvelope("event-stable", "business-stable"))
	if err != nil {
		t.Fatalf("准备 Outbox 事件失败：%v", err)
	}

	now := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC)
	firstPublisher := &recordingPublisher{}
	first, err := outbox.NewRelay(db, firstPublisher, zap.NewNop(), outbox.RelayOptions{
		Owner: "relay-a", BatchSize: 1, LeaseDuration: time.Second, Now: func() time.Time { return now },
	})
	if err != nil {
		t.Fatalf("创建首个 Relay 失败：%v", err)
	}
	claimed, err := first.ClaimBatch(context.Background())
	if err != nil || len(claimed) != 1 {
		t.Fatalf("首个 Relay 领取失败：%v，数量：%d", err, len(claimed))
	}
	// 模拟入队成功但数据库标记前崩溃：公开队列信封已经可观察，但租约没有完成。
	if err := firstPublisher.Publish(context.Background(), deliveryFromModel(claimed[0])); err != nil {
		t.Fatalf("模拟首次入队失败：%v", err)
	}

	now = now.Add(2 * time.Second)
	secondPublisher := &recordingPublisher{}
	second, err := outbox.NewRelay(db, secondPublisher, zap.NewNop(), outbox.RelayOptions{
		Owner: "relay-b", BatchSize: 1, LeaseDuration: time.Minute, Now: func() time.Time { return now },
	})
	if err != nil {
		t.Fatalf("创建恢复 Relay 失败：%v", err)
	}
	processed, err := second.ProcessBatch(context.Background())
	if err != nil || processed != 1 {
		t.Fatalf("恢复投递失败：%v，数量：%d", err, processed)
	}
	if len(firstPublisher.envelopes) != 1 || len(secondPublisher.envelopes) != 1 {
		t.Fatalf("至少一次投递次数不正确：%d/%d", len(firstPublisher.envelopes), len(secondPublisher.envelopes))
	}
	firstEnvelope := firstPublisher.envelopes[0]
	secondEnvelope := secondPublisher.envelopes[0]
	if firstEnvelope.EventID != event.EventID || secondEnvelope.EventID != event.EventID ||
		firstEnvelope.CorrelationID != secondEnvelope.CorrelationID || string(firstEnvelope.Payload) != string(secondEnvelope.Payload) {
		t.Fatalf("恢复投递改变了事件身份或载荷：%+v / %+v", firstEnvelope, secondEnvelope)
	}
}

func TestRelayFailureBackoffAndFinalFailure(t *testing.T) {
	db := testutil.NewPostgresTransaction(t)
	createTemporaryOutboxTables(t, db)
	repository := outbox.NewRepository()
	event, err := repository.Append(context.Background(), db, newEnvelope("event-retry", "business-retry"))
	if err != nil {
		t.Fatalf("准备 Outbox 事件失败：%v", err)
	}
	if err := db.Model(&model.OutboxEvent{}).Where("id = ?", event.ID).Update("max_retries", 2).Error; err != nil {
		t.Fatalf("设置最大重试次数失败：%v", err)
	}

	now := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC)
	publisher := &recordingPublisher{err: stderrors.New("测试队列失败")}
	relay, err := outbox.NewRelay(db, publisher, zap.NewNop(), outbox.RelayOptions{
		Owner: "relay-retry", BatchSize: 1, LeaseDuration: time.Minute, Now: func() time.Time { return now },
	})
	if err != nil {
		t.Fatalf("创建 Relay 失败：%v", err)
	}
	if _, err := relay.ProcessBatch(context.Background()); err != nil {
		t.Fatalf("记录首次失败失败：%v", err)
	}
	var afterFirst model.OutboxEvent
	if err := db.First(&afterFirst, event.ID).Error; err != nil {
		t.Fatalf("读取首次失败事实失败：%v", err)
	}
	if afterFirst.Status != constants.OutboxStatusPending || afterFirst.RetryCount != 1 || !afterFirst.NextAttemptAt.After(now) {
		t.Fatalf("首次失败未按退避重试：%+v", afterFirst)
	}

	now = afterFirst.NextAttemptAt
	if _, err := relay.ProcessBatch(context.Background()); err != nil {
		t.Fatalf("记录最终失败失败：%v", err)
	}
	var final model.OutboxEvent
	if err := db.First(&final, event.ID).Error; err != nil {
		t.Fatalf("读取最终失败事实失败：%v", err)
	}
	if final.Status != constants.OutboxStatusFailed || final.RetryCount != 2 || final.LastErrorSummary != "队列暂时不可用" {
		t.Fatalf("最终失败事实不正确：%+v", final)
	}
}

func TestRelayPermanentFailureStopsRetryImmediately(t *testing.T) {
	db := testutil.NewPostgresTransaction(t)
	createTemporaryOutboxTables(t, db)
	repository := outbox.NewRepository()
	event, err := repository.Append(context.Background(), db, newEnvelope("event-permanent", "business-permanent"))
	if err != nil {
		t.Fatalf("准备永久失败事件失败：%v", err)
	}
	now := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC)
	publisher := &recordingPublisher{err: outbox.Permanent(stderrors.New("载荷版本不受支持"))}
	relay, err := outbox.NewRelay(db, publisher, zap.NewNop(), outbox.RelayOptions{
		Owner: "relay-permanent", BatchSize: 1, LeaseDuration: time.Minute, Now: func() time.Time { return now },
	})
	if err != nil {
		t.Fatalf("创建 Relay 失败：%v", err)
	}
	if _, err := relay.ProcessBatch(context.Background()); err != nil {
		t.Fatalf("记录永久失败事实失败：%v", err)
	}
	var failed model.OutboxEvent
	if err := db.First(&failed, event.ID).Error; err != nil {
		t.Fatalf("读取永久失败事实失败：%v", err)
	}
	if failed.Status != constants.OutboxStatusFailed || failed.RetryCount != 1 || failed.LastErrorCode != "OUTBOX_PERMANENT_FAILURE" {
		t.Fatalf("永久失败未立即停止重试：%+v", failed)
	}
}

type recordingConsumer struct {
	envelope outbox.DeliveryEnvelope
}

func (c *recordingConsumer) Consume(_ context.Context, envelope outbox.DeliveryEnvelope) error {
	c.envelope = envelope
	return nil
}

func TestPublicAsynqHandlerObservesStructuredEnvelope(t *testing.T) {
	t.Parallel()

	envelope := outbox.DeliveryEnvelope{
		EventID: "event-handler", EventType: "foundation.example.created", PayloadVersion: 1,
		CorrelationID: "correlation-handler", Payload: sonic.NoCopyRawMessage(`{"visible":true}`),
	}
	payload, err := sonic.Marshal(envelope)
	if err != nil {
		t.Fatalf("序列化公开信封失败：%v", err)
	}
	consumer := &recordingConsumer{}
	handler := outbox.NewHandler(consumer)
	if err := handler.Handle(context.Background(), asynq.NewTask(constants.TaskTypeOutboxDeliver, payload)); err != nil {
		t.Fatalf("公开 Handler 处理失败：%v", err)
	}
	if consumer.envelope.EventID != envelope.EventID || consumer.envelope.CorrelationID != envelope.CorrelationID {
		t.Fatalf("公开 Handler 未原样传播身份：%+v", consumer.envelope)
	}
}

func TestPostgresRelayRedisAsynqAndPublicHandlerChain(t *testing.T) {
	db := testutil.NewPostgresTransaction(t)
	redisClient := testutil.NewRedisClient(t)
	createTemporaryOutboxTables(t, db)

	repository := outbox.NewRepository()
	event, err := repository.Append(context.Background(), db, newEnvelope("event-real-chain", "business-real-chain"))
	if err != nil {
		t.Fatalf("准备真实链路事件失败：%v", err)
	}
	queueClient := queue.NewClient(redisClient, zap.NewNop())
	t.Cleanup(func() { _ = queueClient.Close() })
	publisher := outbox.NewQueuePublisher(queueClient)
	relay, err := outbox.NewRelay(db, publisher, zap.NewNop(), outbox.RelayOptions{Owner: "relay-real-chain", BatchSize: 1})
	if err != nil {
		t.Fatalf("创建真实链路 Relay 失败：%v", err)
	}
	processed, err := relay.ProcessBatch(context.Background())
	if err != nil || processed != 1 {
		t.Fatalf("真实链路投递失败：%v，数量：%d", err, processed)
	}

	options := redisClient.Options()
	inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: options.Addr, Password: options.Password, DB: options.DB})
	t.Cleanup(func() { _ = inspector.Close() })
	tasks, err := inspector.ListPendingTasks(constants.QueueOutboxDeliver, asynq.PageSize(1000))
	if err != nil {
		t.Fatalf("检查 Asynq 待处理任务失败：%v", err)
	}
	var matched *asynq.TaskInfo
	for _, info := range tasks {
		var queued outbox.DeliveryEnvelope
		if sonic.Unmarshal(info.Payload, &queued) == nil && queued.EventID == event.EventID {
			matched = info
			break
		}
	}
	if matched == nil {
		t.Fatal("真实 Asynq 队列中未找到本次公共事件")
	}
	t.Cleanup(func() { _ = inspector.DeleteTask(matched.Queue, matched.ID) })

	consumer := &recordingConsumer{}
	handler := outbox.NewHandler(consumer)
	if err := handler.Handle(context.Background(), asynq.NewTask(matched.Type, matched.Payload)); err != nil {
		t.Fatalf("公开 Handler 处理真实队列载荷失败：%v", err)
	}
	if consumer.envelope.EventID != event.EventID || consumer.envelope.CorrelationID != event.CorrelationID {
		t.Fatalf("真实链路未原样传播事件身份：%+v", consumer.envelope)
	}

	var delivered model.OutboxEvent
	if err := db.First(&delivered, event.ID).Error; err != nil {
		t.Fatalf("读取已投递事件失败：%v", err)
	}
	if delivered.Status != constants.OutboxStatusDelivered || delivered.DeliveredAt == nil {
		t.Fatalf("真实链路未完成 Outbox 状态：%+v", delivered)
	}
}

func deliveryFromModel(event model.OutboxEvent) outbox.DeliveryEnvelope {
	return outbox.DeliveryEnvelope{
		EventID: event.EventID, EventType: event.EventType, PayloadVersion: event.PayloadVersion,
		AggregateType: event.AggregateType, AggregateID: event.AggregateID,
		ResourceType: event.ResourceType, ResourceID: event.ResourceID, BusinessKey: event.BusinessKey,
		RequestID: event.RequestID, CorrelationID: event.CorrelationID,
		Payload: sonic.NoCopyRawMessage(event.Payload),
	}
}
