package transaction

import (
	"context"
	"errors"
	"testing"

	"go.uber.org/zap"
	"go.uber.org/zap/zaptest"
)

// TestNewSaga verifies that NewSaga creates a properly initialized Saga instance
func TestNewSaga(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	if saga == nil {
		t.Fatal("NewSaga returned nil")
	}

	if saga.name != "test-saga" {
		t.Errorf("expected name 'test-saga', got '%s'", saga.name)
	}

	if saga.compensators == nil {
		t.Error("compensators slice is nil")
	}

	if len(saga.compensators) != 0 {
		t.Errorf("expected 0 compensators, got %d", len(saga.compensators))
	}

	if saga.logger == nil {
		t.Error("logger is nil")
	}
}

// TestAddCompensation_Single verifies that adding a single compensation works
func TestAddCompensation_Single(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	executed := false
	compensator := func(ctx context.Context) error {
		executed = true
		return nil
	}

	saga.AddCompensation(compensator)

	if len(saga.compensators) != 1 {
		t.Errorf("expected 1 compensator, got %d", len(saga.compensators))
	}

	// Verify compensator can be called
	ctx := context.Background()
	if err := saga.compensators[0](ctx); err != nil {
		t.Errorf("compensator returned error: %v", err)
	}

	if !executed {
		t.Error("compensator was not executed")
	}
}

// TestAddCompensation_Multiple verifies that adding multiple compensations works
func TestAddCompensation_Multiple(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	// Add three compensations
	for i := 0; i < 3; i++ {
		saga.AddCompensation(func(ctx context.Context) error {
			return nil
		})
	}

	if len(saga.compensators) != 3 {
		t.Errorf("expected 3 compensators, got %d", len(saga.compensators))
	}
}

// TestRollback_EmptyCompensations verifies that rollback with no compensations is safe
func TestRollback_EmptyCompensations(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	ctx := context.Background()

	// Should not panic or error
	saga.Rollback(ctx)
}

// TestRollback_SingleCompensation_Success verifies successful rollback with one compensation
func TestRollback_SingleCompensation_Success(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	executed := false
	saga.AddCompensation(func(ctx context.Context) error {
		executed = true
		return nil
	})

	ctx := context.Background()
	saga.Rollback(ctx)

	if !executed {
		t.Error("compensation was not executed during rollback")
	}
}

// TestRollback_SingleCompensation_Failure verifies rollback continues on compensation failure
func TestRollback_SingleCompensation_Failure(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	expectedError := errors.New("compensation failed")
	executed := false

	saga.AddCompensation(func(ctx context.Context) error {
		executed = true
		return expectedError
	})

	ctx := context.Background()
	saga.Rollback(ctx)

	if !executed {
		t.Error("compensation was not executed during rollback")
	}

	// Rollback should not panic or return error, just log it
}

// TestRollback_LIFO_ExecutionOrder verifies compensations execute in reverse order
func TestRollback_LIFO_ExecutionOrder(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	var executionOrder []int

	// Add three compensations
	for i := 1; i <= 3; i++ {
		index := i // Capture value in local variable
		saga.AddCompensation(func(ctx context.Context) error {
			executionOrder = append(executionOrder, index)
			return nil
		})
	}

	ctx := context.Background()
	saga.Rollback(ctx)

	// Verify LIFO order: should be [3, 2, 1]
	expected := []int{3, 2, 1}
	if len(executionOrder) != len(expected) {
		t.Fatalf("expected %d executions, got %d", len(expected), len(executionOrder))
	}

	for i, v := range executionOrder {
		if v != expected[i] {
			t.Errorf("execution order[%d]: expected %d, got %d", i, expected[i], v)
		}
	}
}

// TestRollback_MultipleCompensations_AllSuccess verifies all compensations execute
func TestRollback_MultipleCompensations_AllSuccess(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	executedCount := 0

	// Add 5 compensations
	for i := 0; i < 5; i++ {
		saga.AddCompensation(func(ctx context.Context) error {
			executedCount++
			return nil
		})
	}

	ctx := context.Background()
	saga.Rollback(ctx)

	if executedCount != 5 {
		t.Errorf("expected 5 compensations executed, got %d", executedCount)
	}
}

// TestRollback_MultipleCompensations_PartialFailure verifies best-effort behavior
func TestRollback_MultipleCompensations_PartialFailure(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	executedCount := 0

	// Add 5 compensations, second one fails
	for i := 0; i < 5; i++ {
		index := i
		saga.AddCompensation(func(ctx context.Context) error {
			executedCount++
			if index == 1 {
				return errors.New("compensation 2 failed")
			}
			return nil
		})
	}

	ctx := context.Background()
	saga.Rollback(ctx)

	// All 5 should be attempted despite failure
	if executedCount != 5 {
		t.Errorf("expected 5 compensations attempted, got %d", executedCount)
	}
}

// TestRollback_ContextCancellation verifies context is passed to compensations
func TestRollback_ContextCancellation(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	receivedContext := false
	saga.AddCompensation(func(ctx context.Context) error {
		if ctx != nil {
			receivedContext = true
		}
		return nil
	})

	ctx := context.Background()
	saga.Rollback(ctx)

	if !receivedContext {
		t.Error("compensation did not receive context")
	}
}

// TestRollback_CancelledContext verifies compensations see cancelled context
func TestRollback_CancelledContext(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	ctxCancelled := false
	saga.AddCompensation(func(ctx context.Context) error {
		select {
		case <-ctx.Done():
			ctxCancelled = true
		default:
			// Context not cancelled
		}
		return nil
	})

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel context before rollback

	saga.Rollback(ctx)

	if !ctxCancelled {
		t.Error("compensation did not receive cancelled context")
	}
}

// TestMustRollback verifies MustRollback behaves like Rollback
func TestMustRollback(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	executed := false
	saga.AddCompensation(func(ctx context.Context) error {
		executed = true
		return nil
	})

	ctx := context.Background()
	saga.MustRollback(ctx)

	if !executed {
		t.Error("MustRollback did not execute compensation")
	}
}

// TestMustRollback_DoesNotPanic verifies MustRollback doesn't panic on failure
func TestMustRollback_DoesNotPanic(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	saga.AddCompensation(func(ctx context.Context) error {
		return errors.New("compensation failed")
	})

	ctx := context.Background()

	// Should not panic
	defer func() {
		if r := recover(); r != nil {
			t.Errorf("MustRollback panicked: %v", r)
		}
	}()

	saga.MustRollback(ctx)
}

// TestRollback_VariableCaptureWarning demonstrates the closure gotcha
func TestRollback_VariableCaptureWarning(t *testing.T) {
	logger := zaptest.NewLogger(t)

	// Simulate loop with variable capture issue
	var executedIDs []int

	// WRONG: Capturing loop variable directly
	type Resource struct {
		ID int
	}

	resources := []Resource{{ID: 1}, {ID: 2}, {ID: 3}}

	sagaWrong := NewSaga("wrong-capture", logger)
	for _, resource := range resources {
		// This is WRONG but we're demonstrating the problem
		sagaWrong.AddCompensation(func(ctx context.Context) error {
			// Will always use the last value of 'resource'
			executedIDs = append(executedIDs, resource.ID)
			return nil
		})
	}

	ctx := context.Background()
	sagaWrong.Rollback(ctx)

	// All compensations will use resource.ID = 3 (the last value)
	for _, id := range executedIDs {
		if id != 3 {
			t.Logf("Warning: captured variable changed (this is expected in wrong usage)")
		}
	}

	// CORRECT: Capture value in local variable
	executedIDsCorrect := []int{}
	sagaCorrect := NewSaga("correct-capture", logger)

	for _, resource := range resources {
		resourceID := resource.ID // Capture value
		sagaCorrect.AddCompensation(func(ctx context.Context) error {
			executedIDsCorrect = append(executedIDsCorrect, resourceID)
			return nil
		})
	}

	sagaCorrect.Rollback(ctx)

	// Should execute in reverse: [3, 2, 1]
	expected := []int{3, 2, 1}
	if len(executedIDsCorrect) != len(expected) {
		t.Fatalf("expected %d executions, got %d", len(expected), len(executedIDsCorrect))
	}

	for i, id := range executedIDsCorrect {
		if id != expected[i] {
			t.Errorf("execution[%d]: expected %d, got %d", i, expected[i], id)
		}
	}
}

// TestRollback_Idempotency verifies compensations can be called multiple times
func TestRollback_Idempotency(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	executionCount := 0
	saga.AddCompensation(func(ctx context.Context) error {
		executionCount++
		// Idempotent: safe to call multiple times
		return nil
	})

	ctx := context.Background()

	// Call rollback twice
	saga.Rollback(ctx)
	saga.Rollback(ctx)

	// Should execute twice (once per rollback call)
	if executionCount != 2 {
		t.Errorf("expected 2 executions, got %d", executionCount)
	}
}

// TestRollback_RealWorldScenario simulates a registration flow
func TestRollback_RealWorldScenario(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("user-registration", logger)
	ctx := context.Background()

	// Track what got created and deleted
	tenantCreated := false
	tenantDeleted := false
	userCreated := false
	userDeleted := false

	// Step 1: Create tenant
	tenantID := "tenant-123"
	tenantCreated = true

	saga.AddCompensation(func(ctx context.Context) error {
		tenantDeleted = true
		return nil
	})

	// Step 2: Create user
	userID := "user-456"
	userCreated = true

	saga.AddCompensation(func(ctx context.Context) error {
		userDeleted = true
		return nil
	})

	// Step 3: Something fails (e.g., email sending)
	emailErr := errors.New("email service unavailable")
	if emailErr != nil {
		// Rollback should delete user then tenant (LIFO)
		saga.Rollback(ctx)
	}

	// Verify cleanup happened
	if !tenantCreated {
		t.Error("tenant was not created")
	}
	if !userCreated {
		t.Error("user was not created")
	}
	if !userDeleted {
		t.Error("user was not deleted during rollback")
	}
	if !tenantDeleted {
		t.Error("tenant was not deleted during rollback")
	}

	// Verify IDs are still accessible (not used in this test but good practice)
	_ = tenantID
	_ = userID
}

// TestRollback_NoCompensationsRegistered_NoOp verifies rollback is safe when nothing registered
func TestRollback_NoCompensationsRegistered_NoOp(t *testing.T) {
	logger := zaptest.NewLogger(t)
	saga := NewSaga("test-saga", logger)

	ctx := context.Background()

	// Should be a no-op, no panic
	saga.Rollback(ctx)
	saga.MustRollback(ctx)
}

// BenchmarkSaga_AddCompensation benchmarks compensation registration
func BenchmarkSaga_AddCompensation(b *testing.B) {
	logger := zap.NewNop()
	saga := NewSaga("benchmark-saga", logger)

	compensator := func(ctx context.Context) error {
		return nil
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		saga.AddCompensation(compensator)
	}
}

// BenchmarkSaga_Rollback benchmarks rollback execution
func BenchmarkSaga_Rollback(b *testing.B) {
	logger := zap.NewNop()
	ctx := context.Background()

	// Prepare saga with 10 compensations
	compensator := func(ctx context.Context) error {
		return nil
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		saga := NewSaga("benchmark-saga", logger)
		for j := 0; j < 10; j++ {
			saga.AddCompensation(compensator)
		}
		saga.Rollback(ctx)
	}
}

// BenchmarkSaga_RollbackWithFailures benchmarks rollback with compensation failures
func BenchmarkSaga_RollbackWithFailures(b *testing.B) {
	logger := zap.NewNop()
	ctx := context.Background()

	successCompensator := func(ctx context.Context) error {
		return nil
	}

	failureCompensator := func(ctx context.Context) error {
		return errors.New("compensation failed")
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		saga := NewSaga("benchmark-saga", logger)
		for j := 0; j < 5; j++ {
			saga.AddCompensation(successCompensator)
			saga.AddCompensation(failureCompensator)
		}
		saga.Rollback(ctx)
	}
}
