// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package sidx

import (
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPartWrapper_BasicLifecycle(t *testing.T) {
	// Create a mock part
	p := &part{
		path:         "/test/part/001",
		partMetadata: &partMetadata{ID: 1},
	}

	// Create wrapper
	pw := newPartWrapper(nil, p)
	require.NotNil(t, pw)
	assert.Equal(t, int32(1), pw.refCount())
	assert.Equal(t, uint64(1), pw.ID())

	// Test acquire
	assert.True(t, pw.acquire())
	assert.Equal(t, int32(2), pw.refCount())

	// Test multiple acquires
	assert.True(t, pw.acquire())
	assert.True(t, pw.acquire())
	assert.Equal(t, int32(4), pw.refCount())

	// Test releases
	pw.release()
	assert.Equal(t, int32(3), pw.refCount())
	pw.release()
	assert.Equal(t, int32(2), pw.refCount())
	pw.release()
	assert.Equal(t, int32(1), pw.refCount())

	// Final release should trigger cleanup
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())
}

func TestPartWrapper_MarkForRemovalBehavior(t *testing.T) {
	p := &part{
		path:         "/test/part/002",
		partMetadata: &partMetadata{ID: 2},
	}

	pw := newPartWrapper(nil, p)

	// Initial reference count should be 1
	assert.Equal(t, int32(1), pw.refCount())

	// Mark for removal - should set removable flag
	pw.markForRemoval()
	assert.True(t, pw.removable.Load())

	// Should still be able to acquire references until ref count reaches 0
	assert.True(t, pw.acquire())
	assert.Equal(t, int32(2), pw.refCount())

	// Release back to 1
	pw.release()
	assert.Equal(t, int32(1), pw.refCount())

	// Final release should trigger cleanup
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())
}

func TestPartWrapper_ConcurrentReferenceCounting(t *testing.T) {
	p := &part{
		path:         "/test/part/003",
		partMetadata: &partMetadata{ID: 3},
	}

	pw := newPartWrapper(nil, p)

	const numGoroutines = 100
	const operationsPerGoroutine = 1000

	var wg sync.WaitGroup
	var successfulAcquires int64
	var successfulReleases int64

	// Start multiple goroutines that acquire and release references
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < operationsPerGoroutine; j++ {
				if pw.acquire() {
					atomic.AddInt64(&successfulAcquires, 1)
					// Hold the reference briefly
					time.Sleep(time.Microsecond)
					pw.release()
					atomic.AddInt64(&successfulReleases, 1)
				}
			}
		}()
	}

	wg.Wait()

	// All successful acquires should have corresponding releases
	assert.Equal(t, successfulAcquires, successfulReleases)

	// Reference count should be back to 1 (initial reference)
	assert.Equal(t, int32(1), pw.refCount())

	// Final cleanup
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())
}

func TestPartWrapper_ConcurrentAcquireWithMarkForRemoval(t *testing.T) {
	p := &part{
		path:         "/test/part/004",
		partMetadata: &partMetadata{ID: 4},
	}

	pw := newPartWrapper(nil, p)

	const numGoroutines = 20
	var wg sync.WaitGroup
	var successfulAcquires int64
	var failedAcquires int64
	var startBarrier sync.WaitGroup

	startBarrier.Add(1) // Barrier to synchronize goroutine start

	// Start goroutines trying to acquire references
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			startBarrier.Wait() // Wait for all goroutines to be ready
			for j := 0; j < 500; j++ {
				if pw.acquire() {
					atomic.AddInt64(&successfulAcquires, 1)
					time.Sleep(time.Microsecond)
					pw.release()
				} else {
					atomic.AddInt64(&failedAcquires, 1)
				}
			}
		}()
	}

	// Wait a bit, then mark for removal while goroutines are running
	time.Sleep(5 * time.Millisecond)
	pw.markForRemoval()
	startBarrier.Done() // Release all goroutines to start working

	wg.Wait()

	t.Logf("Successful acquires: %d, Failed acquires: %d",
		successfulAcquires, failedAcquires)

	// Should have some failed acquires after marking for removal
	// Note: This may be 0 if all goroutines acquired before markForRemoval,
	// which is acceptable behavior
	t.Logf("Failed acquires: %d (may be 0 due to timing)", failedAcquires)

	// Reference count should be 1 (initial reference)
	assert.Equal(t, int32(1), pw.refCount())

	// Final cleanup
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())
}

func TestPartWrapper_NilPart(t *testing.T) {
	pw := newPartWrapper(nil, nil)
	require.NotNil(t, pw)

	assert.Equal(t, int32(1), pw.refCount())
	assert.Nil(t, pw.p)

	// Test acquire/release with nil part
	assert.True(t, pw.acquire())
	assert.Equal(t, int32(2), pw.refCount())

	pw.release()
	assert.Equal(t, int32(1), pw.refCount())

	pw.release()
	assert.Equal(t, int32(0), pw.refCount())
}

func TestPartWrapper_MultipleReleases(t *testing.T) {
	p := &part{
		path:         "/test/part/005",
		partMetadata: &partMetadata{ID: 5},
	}

	pw := newPartWrapper(nil, p)

	// Test multiple releases before cleanup
	pw.acquire() // ref count = 2
	pw.acquire() // ref count = 3

	pw.release() // ref count = 2
	assert.Equal(t, int32(2), pw.refCount())

	pw.release() // ref count = 1
	assert.Equal(t, int32(1), pw.refCount())

	// Final release should trigger cleanup and set ref to 0
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())

	// Additional releases should not cause issues (though they log warnings)
	pw.release()
	pw.release()
	assert.Equal(t, int32(-2), pw.refCount()) // Goes negative but doesn't break
}

func TestPartWrapper_StringRepresentation(t *testing.T) {
	// Test with nil part
	pw1 := newPartWrapper(nil, nil)
	str1 := pw1.String()
	assert.Contains(t, str1, "id=nil")
	assert.Contains(t, str1, "ref=1")

	// Test with real part
	p := &part{
		path:         "/test/part/006",
		partMetadata: &partMetadata{ID: 6},
	}
	pw2 := newPartWrapper(nil, p)
	str2 := pw2.String()
	assert.Contains(t, str2, "id=6")
	assert.Contains(t, str2, "ref=1")
	assert.Contains(t, str2, "path=/test/part/006")

	// Test reference count changes in string
	pw2.acquire()
	str3 := pw2.String()
	assert.Contains(t, str3, "ref=2")

	pw2.release()
	str4 := pw2.String()
	assert.Contains(t, str4, "ref=1")
}

func TestPartWrapper_StringRepresentationWithMemPart(t *testing.T) {
	// Test with memory part
	mp := &memPart{
		partMetadata: &partMetadata{ID: 42},
	}
	pw := newPartWrapper(mp, nil)
	str := pw.String()
	assert.Contains(t, str, "id=42")
	assert.Contains(t, str, "ref=1")
	assert.Contains(t, str, "memPart=true")
}

func TestPartWrapper_CleanupWithRemovableFlag(t *testing.T) {
	p := &part{
		path:         "/test/part/007",
		partMetadata: &partMetadata{ID: 7},
	}

	pw := newPartWrapper(nil, p)

	// Test that removable flag is initially false
	assert.False(t, pw.removable.Load())

	// Mark for removal sets the flag
	pw.markForRemoval()
	assert.True(t, pw.removable.Load())

	// Release should trigger cleanup
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())
}

// Benchmark tests.
func BenchmarkPartWrapper_AcquireRelease(b *testing.B) {
	p := &part{
		path:         "/bench/part",
		partMetadata: &partMetadata{ID: 1},
	}
	pw := newPartWrapper(nil, p)
	defer pw.release() // cleanup

	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			if pw.acquire() {
				pw.release()
			}
		}
	})
}

func BenchmarkPartWrapper_RefCountCheck(b *testing.B) {
	p := &part{
		path:         "/bench/part",
		partMetadata: &partMetadata{ID: 1},
	}
	pw := newPartWrapper(nil, p)
	defer pw.release() // cleanup

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_ = pw.refCount()
	}
}

func TestPartWrapper_CleanupPreventNegativeRefCount(t *testing.T) {
	p := &part{
		path:         "/test/part/008",
		partMetadata: &partMetadata{ID: 8},
	}

	pw := newPartWrapper(nil, p)
	assert.Equal(t, int32(1), pw.refCount())

	// Acquire a reference
	assert.True(t, pw.acquire())
	assert.Equal(t, int32(2), pw.refCount())

	// Release back to 1
	pw.release()
	assert.Equal(t, int32(1), pw.refCount())

	// Final release should trigger cleanup and set ref to 0
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())

	// Additional releases should not make ref count go further negative than logged
	// The implementation allows negative counts but logs warnings
	initialRef := pw.refCount()
	pw.release()
	newRef := pw.refCount()
	assert.Equal(t, initialRef-1, newRef)   // Should decrement by 1
	assert.LessOrEqual(t, newRef, int32(0)) // Should not be positive
}

func TestPartWrapper_CleanupWithConcurrentAccess(t *testing.T) {
	p := &part{
		path:         "/test/part/009",
		partMetadata: &partMetadata{ID: 9},
	}

	pw := newPartWrapper(nil, p)
	const numGoroutines = 50
	var wg sync.WaitGroup

	// Track the minimum reference count observed
	var minRefCount int64 = 1

	// Start goroutines that try to acquire and immediately release
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 100; j++ {
				if pw.acquire() {
					currentRef := atomic.LoadInt32(&pw.ref)
					for {
						current := atomic.LoadInt64(&minRefCount)
						if int64(currentRef) >= current || atomic.CompareAndSwapInt64(&minRefCount, current, int64(currentRef)) {
							break
						}
					}
					pw.release()
				}
			}
		}()
	}

	wg.Wait()

	// Verify that during concurrent access, ref count never went below 1
	// (except during final cleanup)
	assert.GreaterOrEqual(t, minRefCount, int64(1), "Reference count should never go below 1 during normal operation")

	// Final cleanup
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())
}

func TestPartWrapper_CleanupStateTransition(t *testing.T) {
	p := &part{
		path:         "/test/part/010",
		partMetadata: &partMetadata{ID: 10},
	}

	pw := newPartWrapper(nil, p)

	// Verify initial reference count
	assert.Equal(t, int32(1), pw.refCount())

	// Mark for removal should set removable flag but not trigger cleanup yet
	pw.markForRemoval()
	assert.True(t, pw.removable.Load())
	assert.Equal(t, int32(1), pw.refCount()) // Should still be 1

	// Release should trigger cleanup and set ref count to 0
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())
}

func TestPartWrapper_CleanupWithNilPart(t *testing.T) {
	pw := newPartWrapper(nil, nil)

	// Verify cleanup works correctly with nil part
	assert.Equal(t, int32(1), pw.refCount())

	// Mark for removal
	pw.markForRemoval()
	assert.True(t, pw.removable.Load())

	// Release should complete cleanup without issues
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())

	// Additional releases should be handled gracefully
	pw.release()
	assert.Equal(t, int32(-1), pw.refCount())
}

func TestPartWrapper_CleanupRaceCondition(t *testing.T) {
	p := &part{
		path:         "/test/part/011",
		partMetadata: &partMetadata{ID: 11},
	}

	pw := newPartWrapper(nil, p)

	// Acquire multiple references
	assert.True(t, pw.acquire()) // ref = 2
	assert.True(t, pw.acquire()) // ref = 3
	assert.True(t, pw.acquire()) // ref = 4

	var wg sync.WaitGroup
	const numReleasers = 4

	// Start multiple goroutines to release references simultaneously
	for i := 0; i < numReleasers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			pw.release()
		}()
	}

	wg.Wait()

	// All references should be released, cleanup should have occurred exactly once
	assert.Equal(t, int32(0), pw.refCount())

	// Verify that no further operations are possible
	assert.False(t, pw.acquire())
}

func TestPartWrapper_CleanupIdempotency(t *testing.T) {
	p := &part{
		path:         "/test/part/012",
		partMetadata: &partMetadata{ID: 12},
	}

	pw := newPartWrapper(nil, p)

	// Release to trigger cleanup
	pw.release()
	assert.Equal(t, int32(0), pw.refCount())

	// Call cleanup directly multiple times - should be safe
	pw.cleanup()
	pw.cleanup()
	pw.cleanup()

	// Reference count should remain consistent
	assert.Equal(t, int32(0), pw.refCount())
}

func TestPartWrapper_OverlapsKeyRange(t *testing.T) {
	tests := []struct {
		name     string
		partMin  int64
		partMax  int64
		queryMin int64
		queryMax int64
		expected bool
	}{
		{"complete_overlap", 10, 20, 5, 25, true},
		{"part_inside_query", 10, 20, 5, 25, true},
		{"query_inside_part", 5, 25, 10, 20, true},
		{"partial_overlap_left", 10, 20, 15, 25, true},
		{"partial_overlap_right", 10, 20, 5, 15, true},
		{"adjacent_left_no_overlap", 10, 20, 1, 9, false},
		{"adjacent_right_no_overlap", 10, 20, 21, 30, false},
		{"touching_left_boundary", 10, 20, 5, 10, true},
		{"touching_right_boundary", 10, 20, 20, 25, true},
		{"completely_separate_left", 10, 20, 1, 5, false},
		{"completely_separate_right", 10, 20, 25, 30, false},
		{"exact_match", 10, 20, 10, 20, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create part with metadata
			p := &part{
				path: "/test/part/001",
				partMetadata: &partMetadata{
					ID:     1,
					MinKey: tt.partMin,
					MaxKey: tt.partMax,
				},
			}
			pw := newPartWrapper(nil, p)

			result := pw.overlapsKeyRange(tt.queryMin, tt.queryMax)
			assert.Equal(t, tt.expected, result,
				"part[%d,%d] query[%d,%d] should be %v",
				tt.partMin, tt.partMax, tt.queryMin, tt.queryMax, tt.expected)
		})
	}
}

func TestPartWrapper_OverlapsKeyRange_EdgeCases(t *testing.T) {
	t.Run("nil_part", func(t *testing.T) {
		pw := newPartWrapper(nil, nil)
		assert.False(t, pw.overlapsKeyRange(10, 20))
	})

	t.Run("nil_metadata", func(t *testing.T) {
		p := &part{
			path:         "/test/part/001",
			partMetadata: nil,
		}
		pw := newPartWrapper(nil, p)
		// Should return true (safe default) when metadata is unavailable
		assert.True(t, pw.overlapsKeyRange(10, 20))
	})

	t.Run("invalid_query_range", func(t *testing.T) {
		p := &part{
			path: "/test/part/001",
			partMetadata: &partMetadata{
				ID:     1,
				MinKey: 10,
				MaxKey: 20,
			},
		}
		pw := newPartWrapper(nil, p)
		// Query range where min > max should return false
		assert.False(t, pw.overlapsKeyRange(25, 15))
	})
}

func TestPartWrapper_OverlapsTimestampRange(t *testing.T) {
	minTS := int64(100)
	maxTS := int64(200)
	tests := []struct { //nolint:govet // test table struct field order prioritizes readability
		name      string
		partMinTS *int64
		partMaxTS *int64
		queryMin  int64
		queryMax  int64
		expected  bool
	}{
		{name: "overlap", partMinTS: &minTS, partMaxTS: &maxTS, queryMin: 50, queryMax: 150, expected: true},
		{name: "no_overlap_left", partMinTS: &minTS, partMaxTS: &maxTS, queryMin: 1, queryMax: 50, expected: false},
		{name: "no_overlap_right", partMinTS: &minTS, partMaxTS: &maxTS, queryMin: 250, queryMax: 300, expected: false},
		{name: "nil_timestamps_fallback", partMinTS: nil, partMaxTS: nil, queryMin: 1, queryMax: 1000, expected: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := &part{
				path: "/test/part/001",
				partMetadata: &partMetadata{
					ID:           1,
					MinKey:       1,
					MaxKey:       1000,
					MinTimestamp: tt.partMinTS,
					MaxTimestamp: tt.partMaxTS,
				},
			}
			pw := newPartWrapper(nil, p)
			assert.Equal(t, tt.expected, pw.overlapsTimestampRange(tt.queryMin, tt.queryMax))
		})
	}
}
