package storage

import (
	"testing"

	"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
	"github.com/seaweedfs/seaweedfs/weed/storage/needle"
	"github.com/seaweedfs/seaweedfs/weed/storage/types"
)

func TestHasFreeDiskLocation(t *testing.T) {
	testCases := []struct {
		name           string
		isDiskSpaceLow bool
		maxVolumeCount int32
		currentVolumes int
		expected       bool
	}{
		{
			name:           "low disk space prevents allocation",
			isDiskSpaceLow: true,
			maxVolumeCount: 10,
			currentVolumes: 5,
			expected:       false,
		},
		{
			name:           "normal disk space and available volume count allows allocation",
			isDiskSpaceLow: false,
			maxVolumeCount: 10,
			currentVolumes: 5,
			expected:       true,
		},
		{
			name:           "volume count at max prevents allocation",
			isDiskSpaceLow: false,
			maxVolumeCount: 2,
			currentVolumes: 2,
			expected:       false,
		},
		{
			name:           "volume count over max prevents allocation",
			isDiskSpaceLow: false,
			maxVolumeCount: 2,
			currentVolumes: 3,
			expected:       false,
		},
		{
			name:           "volume count just under max allows allocation",
			isDiskSpaceLow: false,
			maxVolumeCount: 2,
			currentVolumes: 1,
			expected:       true,
		},
		{
			name:           "max volume count is 0 allows allocation",
			isDiskSpaceLow: false,
			maxVolumeCount: 0,
			currentVolumes: 100,
			expected:       true,
		},
		{
			name:           "max volume count is 0 but low disk space prevents allocation",
			isDiskSpaceLow: true,
			maxVolumeCount: 0,
			currentVolumes: 100,
			expected:       false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// setup
			diskLocation := &DiskLocation{
				volumes:        make(map[needle.VolumeId]*Volume),
				MaxVolumeCount: tc.maxVolumeCount,
			}
			diskLocation.isDiskSpaceLow.Store(tc.isDiskSpaceLow)
			for i := 0; i < tc.currentVolumes; i++ {
				diskLocation.volumes[needle.VolumeId(i+1)] = &Volume{}
			}

			store := &Store{
				Locations: []*DiskLocation{diskLocation},
			}

			// act
			result := store.hasFreeDiskLocation(diskLocation)

			// assert
			if result != tc.expected {
				t.Errorf("Expected hasFreeDiskLocation() = %v; want %v for volumes:%d/%d, lowSpace:%v",
					result, tc.expected, len(diskLocation.volumes), diskLocation.MaxVolumeCount, diskLocation.isDiskSpaceLow.Load())
			}
		})
	}
}

func newTestLocation(maxCount int32, isDiskLow bool, volCount int) *DiskLocation {
	location := &DiskLocation{
		volumes:        make(map[needle.VolumeId]*Volume),
		ecVolumes:      make(map[needle.VolumeId]*erasure_coding.EcVolume),
		MaxVolumeCount: maxCount,
		DiskType:       types.ToDiskType("hdd"),
	}
	location.isDiskSpaceLow.Store(isDiskLow)
	for i := 1; i <= volCount; i++ {
		location.volumes[needle.VolumeId(i)] = &Volume{}
	}
	return location
}

func TestCollectHeartbeatRespectsLowDiskSpace(t *testing.T) {
	diskType := types.ToDiskType("hdd")

	t.Run("low disk space", func(t *testing.T) {
		location := newTestLocation(10, true, 3)
		store := &Store{Locations: []*DiskLocation{location}}

		hb := store.CollectHeartbeat()
		if got := hb.MaxVolumeCounts[string(diskType)]; got != 3 {
			t.Errorf("expected low disk space to cap max volume count to used slots, got %d", got)
		}
	})

	t.Run("normal disk space", func(t *testing.T) {
		location := newTestLocation(10, false, 3)
		store := &Store{Locations: []*DiskLocation{location}}

		hb := store.CollectHeartbeat()
		if got := hb.MaxVolumeCounts[string(diskType)]; got != 10 {
			t.Errorf("expected normal disk space to report configured max volume count, got %d", got)
		}
	})

	t.Run("low disk space zero volumes", func(t *testing.T) {
		location := newTestLocation(10, true, 0)
		store := &Store{Locations: []*DiskLocation{location}}

		hb := store.CollectHeartbeat()
		if got := hb.MaxVolumeCounts[string(diskType)]; got != 0 {
			t.Errorf("expected zero volumes to report zero capacity, got %d", got)
		}
	})

	t.Run("low disk space with ec shards", func(t *testing.T) {
		location := newTestLocation(10, true, 3)

		ecVolume := &erasure_coding.EcVolume{VolumeId: 1}
		const shardCount = 15
		for i := 0; i < shardCount; i++ {
			ecVolume.Shards = append(ecVolume.Shards, &erasure_coding.EcVolumeShard{
				ShardId: erasure_coding.ShardId(i),
			})
		}
		location.ecVolumes[ecVolume.VolumeId] = ecVolume
		store := &Store{Locations: []*DiskLocation{location}}

		hb := store.CollectHeartbeat()
		expectedSlots := len(location.volumes) + (shardCount+erasure_coding.DataShardsCount-1)/erasure_coding.DataShardsCount
		if got := hb.MaxVolumeCounts[string(diskType)]; got != uint32(expectedSlots) {
			t.Errorf("expected low disk space to include ec shard contribution, got %d want %d", got, expectedSlots)
		}
	})

	t.Run("low disk space with multiple ec volumes", func(t *testing.T) {
		location := newTestLocation(10, true, 2)

		totalShardCount := 0

		addEcVolume := func(vid needle.VolumeId, shardCount int) {
			ecVolume := &erasure_coding.EcVolume{VolumeId: vid}
			for i := 0; i < shardCount; i++ {
				ecVolume.Shards = append(ecVolume.Shards, &erasure_coding.EcVolumeShard{
					ShardId: erasure_coding.ShardId(i),
				})
			}
			location.ecVolumes[vid] = ecVolume
			totalShardCount += shardCount
		}

		addEcVolume(1, 12)
		addEcVolume(2, 6)

		store := &Store{Locations: []*DiskLocation{location}}

		hb := store.CollectHeartbeat()
		expectedSlots := len(location.volumes)
		expectedSlots += (totalShardCount + erasure_coding.DataShardsCount - 1) / erasure_coding.DataShardsCount

		if got := hb.MaxVolumeCounts[string(diskType)]; got != uint32(expectedSlots) {
			t.Errorf("expected multiple ec volumes to be counted, got %d want %d", got, expectedSlots)
		}
	})
}
