package dirty

import (
	"sync"
	"testing"

	"gotest.tools/v3/assert"
)

// testValue is a simple cloneable type for testing
type testValue struct {
	data string
}

func (v *testValue) Clone() *testValue {
	return &testValue{data: v.data}
}

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

	t.Run("proxy for race condition", func(t *testing.T) {
		t.Parallel()

		// Create a sync map with a base value
		base := map[string]*testValue{
			"key1": {data: "original"},
		}
		syncMap := NewSyncMap(base)

		// Load the same entry from multiple goroutines to simulate race condition
		var entry1, entry2 *SyncMapEntry[string, *testValue]
		var wg sync.WaitGroup
		wg.Add(2)

		// First goroutine loads the entry
		go func() {
			defer wg.Done()
			var ok bool
			entry1, ok = syncMap.Load("key1")
			assert.Assert(t, ok, "entry1 should be loaded")
		}()

		// Second goroutine loads the same entry
		go func() {
			defer wg.Done()
			var ok bool
			entry2, ok = syncMap.Load("key1")
			assert.Assert(t, ok, "entry2 should be loaded")
		}()

		wg.Wait()

		// Both entries should exist and have the same initial value
		assert.Equal(t, "original", entry1.Value().data)
		assert.Equal(t, "original", entry2.Value().data)
		assert.Equal(t, false, entry1.Dirty())
		assert.Equal(t, false, entry2.Dirty())

		// Now try to change both entries concurrently to trigger the proxy mechanism.
		// (This change doesn't actually have to be concurrent to test the proxy behavior,
		// but might exercise concurrency safety in -race mode.)
		var changeWg sync.WaitGroup
		changeWg.Add(2)

		go func() {
			defer changeWg.Done()
			entry1.Change(func(v *testValue) {
				v.data = "changed_by_entry1"
			})
		}()

		go func() {
			defer changeWg.Done()
			entry2.Change(func(v *testValue) {
				v.data = "changed_by_entry2"
			})
		}()

		changeWg.Wait()

		// After the race, one entry should have proxyFor set and both should reflect the same final state
		// The exact final value depends on which goroutine wins the race, but both entries should be consistent
		finalValue1 := entry1.Value().data
		finalValue2 := entry2.Value().data
		assert.Equal(t, finalValue1, finalValue2, "both entries should have the same final value")

		// Both entries should be marked as dirty
		assert.Equal(t, true, entry1.Dirty())
		assert.Equal(t, true, entry2.Dirty())

		// At least one entry should have proxyFor set (the one that lost the race)
		hasProxy := (entry1.proxyFor != nil) || (entry2.proxyFor != nil)
		assert.Assert(t, hasProxy, "at least one entry should have proxyFor set")

		// If entry1 has a proxy, it should point to entry2, and vice versa
		if entry1.proxyFor != nil {
			assert.Equal(t, entry2, entry1.proxyFor, "entry1 should proxy to entry2")
		}
		if entry2.proxyFor != nil {
			assert.Equal(t, entry1, entry2.proxyFor, "entry2 should proxy to entry1")
		}
	})

	t.Run("proxy operations delegation", func(t *testing.T) {
		t.Parallel()

		base := map[string]*testValue{
			"key1": {data: "original"},
		}
		syncMap := NewSyncMap(base)

		// Load two entries for the same key
		entry1, ok1 := syncMap.Load("key1")
		assert.Assert(t, ok1)
		entry2, ok2 := syncMap.Load("key1")
		assert.Assert(t, ok2)

		// Force one to become a proxy by making them both dirty in sequence
		entry1.Change(func(v *testValue) {
			v.data = "changed_by_entry1"
		})
		entry2.Change(func(v *testValue) {
			v.data = "changed_by_entry2"
		})

		// Determine which is the proxy and which is the target
		var proxy, target *SyncMapEntry[string, *testValue]
		if entry1.proxyFor != nil {
			proxy = entry1
			target = entry2
		} else {
			proxy = entry2
			target = entry1
		}

		// Test that proxy operations are delegated to the target
		// Change through proxy should affect target
		proxy.Change(func(v *testValue) {
			v.data = "changed_through_proxy"
		})
		assert.Equal(t, "changed_through_proxy", target.Value().data)
		assert.Equal(t, "changed_through_proxy", proxy.Value().data)

		// ChangeIf through proxy should work
		changed := proxy.ChangeIf(
			func(v *testValue) bool { return v.data == "changed_through_proxy" },
			func(v *testValue) { v.data = "conditional_change" },
		)
		assert.Assert(t, changed)
		assert.Equal(t, "conditional_change", target.Value().data)
		assert.Equal(t, "conditional_change", proxy.Value().data)

		// Dirty status should be consistent
		assert.Equal(t, target.Dirty(), proxy.Dirty())

		// Locked operations should work through proxy
		proxy.Locked(func(v Value[*testValue]) {
			v.Change(func(val *testValue) {
				val.data = "locked_change"
			})
		})
		assert.Equal(t, "locked_change", target.Value().data)
		assert.Equal(t, "locked_change", proxy.Value().data)
	})

	t.Run("proxy delete operations", func(t *testing.T) {
		t.Parallel()

		base := map[string]*testValue{
			"key1": {data: "original"},
		}
		syncMap := NewSyncMap(base)

		// Load two entries and make one a proxy
		entry1, _ := syncMap.Load("key1")
		entry2, _ := syncMap.Load("key1")

		entry1.Change(func(v *testValue) { v.data = "modified" })
		entry2.Change(func(v *testValue) { v.data = "modified2" })

		// Determine which is the proxy
		var proxy *SyncMapEntry[string, *testValue]
		if entry1.proxyFor != nil {
			proxy = entry1
		} else {
			proxy = entry2
		}

		// Delete through proxy should affect target
		proxy.Delete()

		// Both should reflect the deletion
		_, exists := syncMap.Load("key1")
		assert.Equal(t, false, exists, "key should be deleted from sync map")

		// DeleteIf through proxy should work
		base2 := map[string]*testValue{
			"key2": {data: "test"},
		}
		syncMap2 := NewSyncMap(base2)

		entry3, _ := syncMap2.Load("key2")
		entry4, _ := syncMap2.Load("key2")

		entry3.Change(func(v *testValue) { v.data = "modified" })
		entry4.Change(func(v *testValue) { v.data = "modified2" })

		var proxy2 *SyncMapEntry[string, *testValue]
		if entry3.proxyFor != nil {
			proxy2 = entry3
		} else {
			proxy2 = entry4
		}

		proxy2.DeleteIf(func(v *testValue) bool {
			return v.data == "modified2" || v.data == "modified"
		})

		_, exists2 := syncMap2.Load("key2")
		assert.Equal(t, false, exists2, "key2 should be deleted conditionally")
	})

	t.Run("no proxy when no race", func(t *testing.T) {
		t.Parallel()

		base := map[string]*testValue{
			"key1": {data: "original"},
		}
		syncMap := NewSyncMap(base)

		// Load and modify a single entry - no race condition
		entry, ok := syncMap.Load("key1")
		assert.Assert(t, ok)

		entry.Change(func(v *testValue) {
			v.data = "changed"
		})

		// Should not have a proxy since there was no race
		assert.Assert(t, entry.proxyFor == nil, "entry should not have proxyFor when no race occurs")
		assert.Equal(t, true, entry.Dirty())
		assert.Equal(t, "changed", entry.Value().data)
	})
}
