package topology

import (
	"fmt"
	"testing"

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

// Reproduces https://github.com/seaweedfs/seaweedfs/issues/8986
//
// Topology: 1 DC with 3 racks, replication "010" (different-rack).
// Each volume has replicas on 2 of the 3 racks.
// The third rack has no replica, so ShouldGrowVolumesByDcAndRack
// incorrectly returns true for that rack, causing endless volume creation
// even though the DC already has plenty of non-crowded writable volumes.
var topologyLayout3Racks = `
{
  "dc_edge":{
    "rack1":{
      "server1":{
        "ip":"10.0.0.1",
        "volumes":[
          {"id":1, "size":12312, "replication":"010"},
          {"id":2, "size":12312, "replication":"010"}
        ],
        "limit":30
      }
    },
    "rack2":{
      "server2":{
        "ip":"10.0.0.2",
        "volumes":[
          {"id":1, "size":12312, "replication":"010"},
          {"id":2, "size":12312, "replication":"010"}
        ],
        "limit":30
      }
    },
    "rack3":{
      "server3":{
        "ip":"10.0.0.3",
        "volumes":[],
        "limit":30
      }
    }
  }
}
`

func TestShouldGrowVolumesByDcAndRack_Issue8986(t *testing.T) {
	topo := setup(topologyLayout3Racks)

	rp, _ := super_block.NewReplicaPlacementFromString("010")
	vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)

	writables := vl.CloneWritableVolumes()
	if len(writables) != 2 {
		t.Fatalf("expected 2 writable volumes, got %d", len(writables))
	}

	// rack1 and rack2 each have replicas of the writable volumes.
	// ShouldGrowVolumesByDcAndRack should return false for them.
	if vl.ShouldGrowVolumesByDcAndRack(&writables, "dc_edge", "rack1") {
		t.Error("rack1 should NOT need growth — it has writable volumes")
	}
	if vl.ShouldGrowVolumesByDcAndRack(&writables, "dc_edge", "rack2") {
		t.Error("rack2 should NOT need growth — it has writable volumes")
	}

	// rack3 has no replicas. With the old logic, this returns true (should grow),
	// but since the DC already has non-crowded writable volumes with "010"
	// replication, growing a new volume is unnecessary — the existing volumes
	// can serve any write request that doesn't pin to a specific rack.
	if vl.ShouldGrowVolumesByDcAndRack(&writables, "dc_edge", "rack3") {
		t.Error("rack3 should NOT need growth — the DC already has non-crowded writable volumes that can serve writes")
	}
}

// Topology from https://github.com/seaweedfs/seaweedfs/issues/9832
// 1 DC, 2 racks, 1 server each, replication 010, one crowded volume (28.65 GB)
// near the 30 GB size limit.
var topologyLayout9832 = `
{
  "datacenter1":{
    "rack1":{
      "node-a":{
        "ip":"10.0.0.1",
        "volumes":[ {"id":12, "size":28650, "replication":"010", "collection":"bucket-nexus"} ],
        "limit":30
      }
    },
    "rack2":{
      "node-b":{
        "ip":"10.0.0.2",
        "volumes":[ {"id":12, "size":28650, "replication":"010", "collection":"bucket-nexus"} ],
        "limit":30
      }
    }
  }
}
`

// Reproduces https://github.com/seaweedfs/seaweedfs/issues/9832
//
// A crowded "010" volume made the periodic rack-aware scan return true for
// every rack in the DC, so it grew once per rack with a hardcoded step of 2 —
// 2 racks × 2 = 4 logical volumes (8 physical) — and ignored a lowered
// master.volume_growth.copy_2. PlanRackAwareGrowth now plans a single DC-wide
// grow capped at copy_N.
func TestPlanRackAwareGrowth_Issue9832(t *testing.T) {
	defer restoreCopyCounts(VolumeGrowStrategy.Copy1Count, VolumeGrowStrategy.Copy2Count)
	VolumeGrowStrategy.Copy2Count = 1 // user's master.volume_growth.copy_2

	topo := setupWithLimit(t, topologyLayout9832, 30000)
	rp, _ := super_block.NewReplicaPlacementFromString("010")
	vl := topo.GetVolumeLayout("bucket-nexus", rp, needle.EMPTY_TTL, types.HardDriveType)

	if writables := vl.CloneWritableVolumes(); len(writables) != 1 {
		t.Fatalf("expected 1 writable volume, got %d", len(writables))
	}

	// The crowded volume makes both racks report "should grow" — the source of
	// the old per-rack multiplication.
	writables := vl.CloneWritableVolumes()
	if !vl.ShouldGrowVolumesByDcAndRack(&writables, "datacenter1", "rack1") ||
		!vl.ShouldGrowVolumesByDcAndRack(&writables, "datacenter1", "rack2") {
		t.Fatal("expected both racks to report should-grow for the crowded volume")
	}

	plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2)
	if len(plans) != 1 {
		t.Fatalf("expected 1 DC-wide grow, got %d: %+v", len(plans), plans)
	}
	if plans[0].Rack != "" {
		t.Errorf("expected DC-wide grow (empty rack), got rack %q", plans[0].Rack)
	}
	if plans[0].WritableVolumeCount != 1 {
		t.Errorf("expected copy_2=1 logical volume, got %d", plans[0].WritableVolumeCount)
	}
}

// With the default copy_2 the per-event step is preserved (not increased): the
// fix only removes the per-rack multiplication.
func TestPlanRackAwareGrowth_DefaultStepNotMultiplied(t *testing.T) {
	defer restoreCopyCounts(VolumeGrowStrategy.Copy1Count, VolumeGrowStrategy.Copy2Count)
	VolumeGrowStrategy.Copy2Count = 6 // default

	topo := setupWithLimit(t, topologyLayout9832, 30000)
	rp, _ := super_block.NewReplicaPlacementFromString("010")
	vl := topo.GetVolumeLayout("bucket-nexus", rp, needle.EMPTY_TTL, types.HardDriveType)

	plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2)
	if len(plans) != 1 {
		t.Fatalf("expected 1 DC-wide grow, got %d: %+v", len(plans), plans)
	}
	if plans[0].WritableVolumeCount != 2 {
		t.Errorf("expected step count 2 (min of step 2 and copy_2 6), got %d", plans[0].WritableVolumeCount)
	}
}

// A non-crowded "010" volume needs no growth at all.
func TestPlanRackAwareGrowth_NotCrowdedNoGrowth(t *testing.T) {
	layout := `
{
  "datacenter1":{
    "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":12, "size":1000, "replication":"010", "collection":"c"} ], "limit":30 } },
    "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[ {"id":12, "size":1000, "replication":"010", "collection":"c"} ], "limit":30 } }
  }
}
`
	topo := setupWithLimit(t, layout, 30000)
	rp, _ := super_block.NewReplicaPlacementFromString("010")
	vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)

	if plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2); len(plans) != 0 {
		t.Fatalf("expected no growth for non-crowded volume, got %+v", plans)
	}
}

// Non-rack-spanning replication ("000") still grows per rack: a rack without a
// writable volume gets its own grow.
func TestPlanRackAwareGrowth_PerRackForNonRackSpanning(t *testing.T) {
	layout := `
{
  "datacenter1":{
    "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":1000, "replication":"000", "collection":"c"} ], "limit":30 } },
    "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[], "limit":30 } }
  }
}
`
	topo := setupWithLimit(t, layout, 30000)
	rp, _ := super_block.NewReplicaPlacementFromString("000")
	vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)

	plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2)
	if len(plans) != 1 {
		t.Fatalf("expected 1 per-rack grow, got %d: %+v", len(plans), plans)
	}
	if plans[0].Rack != "rack2" {
		t.Errorf("expected grow pinned to empty rack2, got %q", plans[0].Rack)
	}
}

// lastGrowCount is spread evenly across all grow targets even when DCs have
// different rack counts: each crowded "000" rack gets ceilDiv(lastGrowCount,
// totalRacks), so the total matches the request rather than over-growing per DC.
func TestPlanRackAwareGrowth_EvenDistributionAcrossUnevenDCs(t *testing.T) {
	layout := `
{
  "dc1":{
    "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } },
    "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[ {"id":2, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } }
  },
  "dc2":{
    "rack3":{ "node-c":{ "ip":"10.0.0.3", "volumes":[ {"id":3, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } },
    "rack4":{ "node-d":{ "ip":"10.0.0.4", "volumes":[ {"id":4, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } },
    "rack5":{ "node-e":{ "ip":"10.0.0.5", "volumes":[ {"id":5, "size":28650, "replication":"000", "collection":"c"} ], "limit":30 } }
  }
}
`
	topo := setupWithLimit(t, layout, 30000)
	rp, _ := super_block.NewReplicaPlacementFromString("000")
	vl := topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)

	plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 10, 2)
	if len(plans) != 5 {
		t.Fatalf("expected a grow per crowded rack (5), got %d: %+v", len(plans), plans)
	}
	total := uint32(0)
	for _, p := range plans {
		if p.WritableVolumeCount != 2 { // ceilDiv(10, 5 racks)
			t.Errorf("expected even per-rack count 2, got %d for %s/%s", p.WritableVolumeCount, p.DataCenter, p.Rack)
		}
		total += p.WritableVolumeCount
	}
	if total != 10 {
		t.Errorf("expected total grow 10, got %d", total)
	}
}

// A collection pinned to one data center (fs.configure -dataCenter) lives only
// there, yet the scan used to plan growth for every data center in the
// topology, spreading the collection's volumes across all of them. Data
// centers hosting none of the layout's volumes are not the scan's to fill.
func TestPlanRackAwareGrowth_SkipsDataCentersWithoutLayoutVolumes(t *testing.T) {
	layout := `
{
  "dc1":{ "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":%d, "replication":"000", "collection":"pinned"} ], "limit":30 } } },
  "dc2":{ "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[], "limit":30 } } },
  "dc3":{ "rack3":{ "node-c":{ "ip":"10.0.0.3", "volumes":[], "limit":30 } } },
  "dc4":{ "rack4":{ "node-d":{ "ip":"10.0.0.4", "volumes":[], "limit":30 } } }
}
`
	rp, _ := super_block.NewReplicaPlacementFromString("000")

	topo := setupWithLimit(t, fmt.Sprintf(layout, 1000), 30000)
	vl := topo.GetVolumeLayout("pinned", rp, needle.EMPTY_TTL, types.HardDriveType)
	if plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2); len(plans) != 0 {
		t.Fatalf("healthy dc1 volume: expected no growth, got %+v", plans)
	}

	topo = setupWithLimit(t, fmt.Sprintf(layout, 28650), 30000)
	vl = topo.GetVolumeLayout("pinned", rp, needle.EMPTY_TTL, types.HardDriveType)
	plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 0, 2)
	if len(plans) != 1 {
		t.Fatalf("crowded dc1 volume: expected 1 grow, got %d: %+v", len(plans), plans)
	}
	if plans[0].DataCenter != "dc1" || plans[0].Rack != "rack1" {
		t.Errorf("expected grow pinned to dc1/rack1, got %s/%s", plans[0].DataCenter, plans[0].Rack)
	}
}

// The lastGrowCount divisor counts only the racks the scan can actually plan
// for; racks of skipped data centers would dilute every grow.
func TestPlanRackAwareGrowth_DivisorCountsOnlyHostingRacks(t *testing.T) {
	layout := `
{
  "dc1":{
    "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":28650, "replication":"000", "collection":"pinned"} ], "limit":30 } },
    "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[ {"id":2, "size":28650, "replication":"000", "collection":"pinned"} ], "limit":30 } }
  },
  "dc2":{
    "rack3":{ "node-c":{ "ip":"10.0.0.3", "volumes":[], "limit":30 } },
    "rack4":{ "node-d":{ "ip":"10.0.0.4", "volumes":[], "limit":30 } }
  }
}
`
	topo := setupWithLimit(t, layout, 30000)
	rp, _ := super_block.NewReplicaPlacementFromString("000")
	vl := topo.GetVolumeLayout("pinned", rp, needle.EMPTY_TTL, types.HardDriveType)

	plans := vl.PlanRackAwareGrowth(topo.ListDCAndRacks(), 4, 2)
	if len(plans) != 2 {
		t.Fatalf("expected a grow per crowded dc1 rack, got %d: %+v", len(plans), plans)
	}
	for _, p := range plans {
		if p.DataCenter != "dc1" {
			t.Errorf("expected grows pinned to dc1, got %s/%s", p.DataCenter, p.Rack)
		}
		if p.WritableVolumeCount != 2 { // ceilDiv(4, 2 hosting racks), not ceilDiv(4, 4)
			t.Errorf("expected per-rack count 2, got %d", p.WritableVolumeCount)
		}
	}
}

// The periodic must-grow and crowded growth paths build their request from
// ToVolumeGrowRequest. For a layout living in exactly one data center the
// request carries that DC, so those paths do not scatter a pinned collection
// either; a layout spanning DCs keeps unconstrained growth.
func TestToVolumeGrowRequest_SingleDataCenterLayout(t *testing.T) {
	layout := `
{
  "dc1":{ "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":1000, "replication":"000", "collection":"pinned"} ], "limit":30 } } },
  "dc2":{ "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[ {"id":2, "size":1000, "replication":"000", "collection":"spread"} ], "limit":30 } } },
  "dc3":{ "rack3":{ "node-c":{ "ip":"10.0.0.3", "volumes":[ {"id":3, "size":1000, "replication":"000", "collection":"spread"} ], "limit":30 } } }
}
`
	topo := setupWithLimit(t, layout, 30000)
	rp, _ := super_block.NewReplicaPlacementFromString("000")

	pinned := &VolumeLayoutCollection{"pinned", topo.GetVolumeLayout("pinned", rp, needle.EMPTY_TTL, types.HardDriveType)}
	if vgr := pinned.ToVolumeGrowRequest(); vgr.DataCenter != "dc1" {
		t.Errorf("expected growth pinned to dc1, got %q", vgr.DataCenter)
	}

	spread := &VolumeLayoutCollection{"spread", topo.GetVolumeLayout("spread", rp, needle.EMPTY_TTL, types.HardDriveType)}
	if vgr := spread.ToVolumeGrowRequest(); vgr.DataCenter != "" {
		t.Errorf("expected unconstrained growth for a multi-DC layout, got %q", vgr.DataCenter)
	}
}

// Cross-DC replication can never legitimately live in one data center: a
// single hosting DC there means the other DCs are down, and pinning growth to
// the survivor would encode the outage as a placement constraint.
func TestToVolumeGrowRequest_CrossDCReplicationNeverPinned(t *testing.T) {
	layout := `
{
  "dc1":{ "rack1":{ "node-a":{ "ip":"10.0.0.1", "volumes":[ {"id":1, "size":1000, "replication":"100", "collection":"c"} ], "limit":30 } } },
  "dc2":{ "rack2":{ "node-b":{ "ip":"10.0.0.2", "volumes":[], "limit":30 } } }
}
`
	topo := setupWithLimit(t, layout, 30000)
	rp, _ := super_block.NewReplicaPlacementFromString("100")

	vlc := &VolumeLayoutCollection{"c", topo.GetVolumeLayout("c", rp, needle.EMPTY_TTL, types.HardDriveType)}
	if vgr := vlc.ToVolumeGrowRequest(); vgr.DataCenter != "" {
		t.Errorf("expected unconstrained growth for cross-DC replication, got %q", vgr.DataCenter)
	}
}

// Volumes packed to capacity (e.g. by fs.mergeVolumes) go crowded and then
// unwritable, but stay in the crowded map. ShouldGrowVolumes must count only
// writable crowded volumes, or those leftovers keep writable <= crowded true
// forever and every assign-path grow request passes the gate.
func TestShouldGrowVolumes_UnwritableCrowdedVolumes(t *testing.T) {
	rp, _ := super_block.NewReplicaPlacementFromString("000")
	vl := NewVolumeLayout(rp, needle.EMPTY_TTL, types.HardDriveType, 30000, false)

	vl.accessLock.Lock()
	vl.setVolumeWritable(1)
	vl.setVolumeWritable(2)
	vl.accessLock.Unlock()

	vl.SetVolumeCrowded(1)
	vl.SetVolumeCapacityFull(1)

	if _, crowded := vl.GetWritableVolumeCount(); crowded != 0 {
		t.Fatalf("expected 0 writable crowded volumes, got %d", crowded)
	}
	if vl.ShouldGrowVolumes() {
		t.Fatal("volume 2 still has room, growth is not needed")
	}

	vl.SetVolumeCrowded(2)
	if !vl.ShouldGrowVolumes() {
		t.Fatal("every writable volume is crowded, growth is needed")
	}
}

func restoreCopyCounts(copy1, copy2 uint32) {
	VolumeGrowStrategy.Copy1Count = copy1
	VolumeGrowStrategy.Copy2Count = copy2
}
