package idempotency_test

import (
	"fmt"
	"sync"
	"testing"

	"github.com/agientic/idempotency-go"
	"github.com/stretchr/testify/assert"
)

// ExampleWithIdempotency demonstrates the idempotency pattern.
// This example is executable documentation that Go will verify.
func ExampleWithIdempotency() {
	ctx := idempotency.NewContext()
	args := map[string]any{
		"amount":      100.0,
		"currency":    "USD",
		"customer_id": "cust_123",
	}
	
	// First call - executes
	result1, _ := idempotency.WithIdempotency(ctx, "K", args)
	
	// Second call - returns cached result
	result2, _ := idempotency.WithIdempotency(ctx, "K", args)
	
	// Verify idempotency
	if result1["transaction_id"] == result2["transaction_id"] {
		fmt.Println("Idempotency verified")
	}
	
	// Output: Idempotency verified
}

// TestIdempotencyInvariant tests the core invariant:
// Same key returns same result without re-execution.
// This is the same test across all five language implementations.
func TestIdempotencyInvariant(t *testing.T) {
	ctx := idempotency.NewContext()
	key := "test-key-1"
	args := map[string]any{
		"amount":      100.0,
		"currency":    "USD",
		"customer_id": "cust_123",
	}
	
	// First call - should execute
	result1, err := idempotency.Charge(ctx, key, args)
	assert.NoError(t, err)
	
	// Second call - should return cached result
	result2, err := idempotency.Charge(ctx, key, args)
	assert.NoError(t, err)
	
	// Test assertion (identical across all languages)
	assert.Equal(t, result1, result2, "Same key must return same result")
	
	// Verify exactly one execution
	assert.Equal(t, 1, ctx.LedgerCount("tool.call", key), "Should have exactly 1 call entry")
	assert.Equal(t, 2, ctx.LedgerCount("tool.result", key), "Should have 2 result entries")
	
	// Verify dedup flag
	ledger := ctx.GetLedger()
	var results []map[string]any
	for _, entry := range ledger {
		if entry["kind"] == "tool.result" && entry["idempotencyKey"] == key {
			results = append(results, entry)
		}
	}
	
	assert.Len(t, results, 2)
	assert.Nil(t, results[0]["dedup"])
	assert.True(t, results[1]["dedup"].(bool))
}

func TestDifferentKeysExecuteIndependently(t *testing.T) {
	ctx := idempotency.NewContext()
	args := map[string]any{
		"amount":      100.0,
		"currency":    "USD",
		"customer_id": "cust_123",
	}
	
	result1, err := idempotency.Charge(ctx, "key-1", args)
	assert.NoError(t, err)
	
	result2, err := idempotency.Charge(ctx, "key-2", args)
	assert.NoError(t, err)
	
	// Different keys can have different results
	assert.NotEqual(t, result1["transaction_id"], result2["transaction_id"],
		"Different keys should generate different transaction IDs")
	
	// Each key should have one execution
	assert.Equal(t, 1, ctx.LedgerCount("tool.call", "key-1"))
	assert.Equal(t, 1, ctx.LedgerCount("tool.call", "key-2"))
}

func TestLedgerOrdering(t *testing.T) {
	ctx := idempotency.NewContext()
	key := "test-key-order"
	args := map[string]any{
		"amount":      50.0,
		"currency":    "EUR",
		"customer_id": "cust_456",
	}
	
	_, err := idempotency.Charge(ctx, key, args)
	assert.NoError(t, err)
	
	// Find entries for this key
	ledger := ctx.GetLedger()
	var keyEntries []map[string]any
	for _, entry := range ledger {
		if entry["idempotencyKey"] == key {
			keyEntries = append(keyEntries, entry)
		}
	}
	
	// Should have exactly 2 entries: call, then result
	assert.Len(t, keyEntries, 2)
	assert.Equal(t, "tool.call", keyEntries[0]["kind"])
	assert.Equal(t, "tool.result", keyEntries[1]["kind"])
}

func TestConcurrentRequestsSameKey(t *testing.T) {
	ctx := idempotency.NewContext()
	key := "concurrent-test-key"
	args := map[string]any{
		"amount":      150.0,
		"currency":    "JPY",
		"customer_id": "cust_999",
	}
	
	// Launch multiple concurrent requests
	var wg sync.WaitGroup
	results := make([]map[string]any, 5)
	errors := make([]error, 5)
	
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			result, err := idempotency.Charge(ctx, key, args)
			results[idx] = result
			errors[idx] = err
		}(i)
	}
	
	wg.Wait()
	
	// Check for errors
	for _, err := range errors {
		assert.NoError(t, err)
	}
	
	// All should return the same result
	firstResult := results[0]
	for _, result := range results[1:] {
		assert.Equal(t, firstResult, result, "Concurrent requests must return same result")
	}
	
	// Should still only execute once
	assert.Equal(t, 1, ctx.LedgerCount("tool.call", key),
		"Should execute only once despite concurrency")
}

// BenchmarkIdempotency measures performance of the idempotency pattern.
func BenchmarkIdempotency(b *testing.B) {
	ctx := idempotency.NewContext()
	args := map[string]any{
		"amount":      100.0,
		"currency":    "USD",
		"customer_id": "cust_123",
	}
	
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		key := fmt.Sprintf("bench-key-%d", i%100) // Reuse 100 keys
		_, _ = idempotency.Charge(ctx, key, args)
	}
}