package ecbalancer

import (
	"testing"

	"github.com/seaweedfs/seaweedfs/weed/admin/topology"
	"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
	"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
)

// TestFromActiveTopology verifies the encode/repair-side snapshot constructor maps
// nodes, per-disk EC shard counts, per-volume shard bits, and free slots from an
// ActiveTopology. Shard accounting is asserted exactly; free slots are asserted to
// be positive (their exact value depends on effective-capacity internals).
func TestFromActiveTopology(t *testing.T) {
	const vid uint32 = 7
	at := topology.NewActiveTopology(10)

	// Node A holds one EC shard (id 3) of volume 7 on disk 0; node B is empty.
	nodeA := &master_pb.DataNodeInfo{
		Id: "10.0.0.1:8080",
		DiskInfos: map[string]*master_pb.DiskInfo{
			"hdd": {
				DiskId:         0,
				MaxVolumeCount: 100,
				VolumeCount:    1,
				EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{{
					Id:          vid,
					Collection:  "c1",
					EcIndexBits: uint32(1) << 3,
					DiskId:      0,
				}},
			},
		},
	}
	nodeB := &master_pb.DataNodeInfo{
		Id: "10.0.0.2:8080",
		DiskInfos: map[string]*master_pb.DiskInfo{
			"hdd": {DiskId: 0, MaxVolumeCount: 100, VolumeCount: 0},
		},
	}
	if err := at.UpdateTopology(&master_pb.TopologyInfo{
		DataCenterInfos: []*master_pb.DataCenterInfo{{
			Id: "dc1",
			RackInfos: []*master_pb.RackInfo{{
				Id:            "rack1",
				DataNodeInfos: []*master_pb.DataNodeInfo{nodeA, nodeB},
			}},
		}},
	}); err != nil {
		t.Fatalf("UpdateTopology: %v", err)
	}

	topo := FromActiveTopology(at, 0)

	if got := len(topo.nodes); got != 2 {
		t.Fatalf("node count = %d, want 2", got)
	}

	a := topo.nodes["10.0.0.1:8080"]
	if a == nil {
		t.Fatal("node A missing from snapshot")
	}
	if a.rack != "dc1:rack1" {
		t.Errorf("node A rack = %q, want dc1:rack1", a.rack)
	}
	diskA := a.disks[0]
	if diskA == nil {
		t.Fatal("node A disk 0 missing")
	}
	if diskA.shardCount != 1 {
		t.Errorf("node A disk 0 shardCount = %d, want 1", diskA.shardCount)
	}
	vs := a.shards[volKey{collection: "c1", vid: vid}]
	if vs == nil {
		t.Fatal("volume 7 shards not recorded on node A")
	}
	if !vs.shardBits.Has(erasure_coding.ShardId(3)) {
		t.Errorf("node A volume 7 shardBits %b missing shard 3", vs.shardBits)
	}
	if vs.shardBits.Count() != 1 {
		t.Errorf("node A volume 7 shard count = %d, want 1", vs.shardBits.Count())
	}

	b := topo.nodes["10.0.0.2:8080"]
	if b == nil {
		t.Fatal("node B missing from snapshot")
	}
	if b.rack != "dc1:rack1" {
		t.Errorf("node B rack = %q, want dc1:rack1", b.rack)
	}
	if diskB := b.disks[0]; diskB == nil || diskB.shardCount != 0 {
		t.Errorf("node B disk 0 shardCount = %v, want 0", diskB)
	}
	if len(b.shards) != 0 {
		t.Errorf("node B should hold no volume shards, got %d", len(b.shards))
	}

	// Free slots should be positive on both near-empty disks.
	if a.freeSlots <= 0 || b.freeSlots <= 0 {
		t.Errorf("free slots not positive: A=%d B=%d", a.freeSlots, b.freeSlots)
	}
}

// TestFromActiveTopologyGroupsByAddressHost verifies the snapshot derives a node's
// machine from its address, not its (possibly opaque) id: two volume servers with
// distinct ids but the same host must land on one machine so EC placement spreads
// shards across boxes even when explicit node ids are configured.
func TestFromActiveTopologyGroupsByAddressHost(t *testing.T) {
	at := topology.NewActiveTopology(10)
	mk := func(id, addr string) *master_pb.DataNodeInfo {
		return &master_pb.DataNodeInfo{
			Id:        id,
			Address:   addr,
			DiskInfos: map[string]*master_pb.DiskInfo{"hdd": {DiskId: 0, MaxVolumeCount: 100, VolumeCount: 0}},
		}
	}
	// vs-a and vs-b are two servers on the same physical host; vs-c is another box.
	nodes := []*master_pb.DataNodeInfo{
		mk("vs-a", "10.0.0.9:8080"),
		mk("vs-b", "10.0.0.9:8081"),
		mk("vs-c", "10.0.0.10:8080"),
	}
	if err := at.UpdateTopology(&master_pb.TopologyInfo{
		DataCenterInfos: []*master_pb.DataCenterInfo{{
			Id:        "dc1",
			RackInfos: []*master_pb.RackInfo{{Id: "rack1", DataNodeInfos: nodes}},
		}},
	}); err != nil {
		t.Fatalf("UpdateTopology: %v", err)
	}

	topo := FromActiveTopology(at, 0)
	if topo.nodes["vs-a"].host != topo.nodes["vs-b"].host {
		t.Errorf("vs-a host %q != vs-b host %q; same-machine servers not grouped",
			topo.nodes["vs-a"].host, topo.nodes["vs-b"].host)
	}
	if topo.nodes["vs-a"].host == topo.nodes["vs-c"].host {
		t.Errorf("vs-a and vs-c share host %q but are different machines", topo.nodes["vs-a"].host)
	}
	if got := topo.nodes["vs-a"].host; got != "10.0.0.9" {
		t.Errorf("vs-a host = %q, want 10.0.0.9", got)
	}
}

// TestEcShardSlotsOnDiskRoundsUp covers the mixed-ratio (targetDataShards <
// existingDataShards) conversion: an existing shard's fractional footprint must
// round up so it is never floored to zero, which would overstate free capacity.
// OSS always uses the standard ratio at runtime, but ecShardSlotsOnDisk takes the
// target data-shard count as a parameter, so the fractional path is exercised
// directly here; the enterprise build reaches it with real per-volume ratios.
func TestEcShardSlotsOnDiskRoundsUp(t *testing.T) {
	// A single shard (id 3) of a standard 10-data-shard volume on disk 0.
	disk := &topology.DiskInfo{
		DiskID: 0,
		DiskInfo: &master_pb.DiskInfo{
			DiskId: 0,
			EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{{
				Id:          7,
				Collection:  "c1",
				EcIndexBits: uint32(1) << 3,
				DiskId:      0,
			}},
		},
	}

	// Against a 2-data-shard target the shard occupies 2/10 of a slot, which must
	// round up to 1 rather than floor to 0.
	if got := ecShardSlotsOnDisk(disk, 2); got != 1 {
		t.Errorf("ecShardSlotsOnDisk(target=2) = %d, want 1 (rounded up from 0.2)", got)
	}

	// Identity case: target equals the existing data-shard count, so the shard
	// consumes exactly its whole-number footprint.
	if got := ecShardSlotsOnDisk(disk, erasure_coding.DataShardsCount); got != 1 {
		t.Errorf("ecShardSlotsOnDisk(target=%d) = %d, want 1", erasure_coding.DataShardsCount, got)
	}
}
