package topology

import (
	"testing"

	"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
	"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)

func filterTestTopology(t *testing.T) *Topology {
	t.Helper()
	topo := NewTopology("filter", nil, 32*1024*1024*1024, 5, false)
	dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
		GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})
	topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
		{Id: 1, Collection: "", Size: 1000, Version: 3, DiskId: 2},
		{Id: 2, Collection: "c", Size: 2000, Version: 3, DiskId: 2},
		{Id: 3, Collection: "other", Size: 3000, Version: 3, DiskId: 2},
	}, dn)
	topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
		{Id: 8, Collection: "c", EcIndexBits: 0x3fff, DiskId: 2},
		{Id: 9, Collection: "other", EcIndexBits: 0x3fff, DiskId: 2},
	}, dn)
	return topo
}

// listed returns the volume and ec ids a filtered listing carries.
func listed(info *master_pb.TopologyInfo) (volumes []uint32, ecVolumes []uint32) {
	for _, dc := range info.DataCenterInfos {
		for _, rack := range dc.RackInfos {
			for _, node := range rack.DataNodeInfos {
				for _, disk := range node.DiskInfos {
					for _, v := range disk.VolumeInfos {
						volumes = append(volumes, v.Id)
					}
					for _, ec := range disk.EcShardInfos {
						ecVolumes = append(ecVolumes, ec.Id)
					}
				}
			}
		}
	}
	return
}

func equalIds(got, want []uint32) bool {
	if len(got) != len(want) {
		return false
	}
	seen := map[uint32]int{}
	for _, id := range got {
		seen[id]++
	}
	for _, id := range want {
		seen[id]--
	}
	for _, n := range seen {
		if n != 0 {
			return false
		}
	}
	return true
}

func TestVolumeFilterSelects(t *testing.T) {
	topo := filterTestTopology(t)
	collection := func(name string) *string { return &name }
	volumes := func(ids ...needle.VolumeId) map[needle.VolumeId]struct{} {
		set := make(map[needle.VolumeId]struct{}, len(ids))
		for _, id := range ids {
			set[id] = struct{}{}
		}
		return set
	}

	for _, tc := range []struct {
		name          string
		filter        VolumeFilter
		wantVolumes   []uint32
		wantEcVolumes []uint32
	}{
		{"no filter takes everything", VolumeFilter{}, []uint32{1, 2, 3}, []uint32{8, 9}},
		{"one collection", VolumeFilter{Collection: collection("c")}, []uint32{2}, []uint32{8}},
		// Asking for it must not read as asking for everything.
		{"the default collection", VolumeFilter{Collection: collection("")}, []uint32{1}, nil},
		{"a collection nothing is in", VolumeFilter{Collection: collection("none")}, nil, nil},
		{"one volume", VolumeFilter{VolumeIds: volumes(3)}, []uint32{3}, nil},
		{"one ec volume", VolumeFilter{VolumeIds: volumes(9)}, nil, []uint32{9}},
		{"both, agreeing", VolumeFilter{Collection: collection("other"), VolumeIds: volumes(3)}, []uint32{3}, nil},
		{"both, disagreeing", VolumeFilter{Collection: collection("c"), VolumeIds: volumes(3)}, nil, nil},
	} {
		t.Run(tc.name, func(t *testing.T) {
			volumes, ecVolumes := listed(topo.ToTopologyInfo(tc.filter))
			if !equalIds(volumes, tc.wantVolumes) {
				t.Errorf("listed volumes %v, want %v", volumes, tc.wantVolumes)
			}
			if !equalIds(ecVolumes, tc.wantEcVolumes) {
				t.Errorf("listed ec volumes %v, want %v", ecVolumes, tc.wantEcVolumes)
			}
		})
	}
}

// an id nothing answers to narrows the listing rather than widening it.
func TestVolumeFilterVolumeIds(t *testing.T) {
	topo := filterTestTopology(t)

	for _, tc := range []struct {
		name          string
		ids           []needle.VolumeId
		wantVolumes   []uint32
		wantEcVolumes []uint32
	}{
		// No id asked of the filter, so every volume is listed.
		{"no id asked", nil, []uint32{1, 2, 3}, []uint32{8, 9}},
		{"one regular volume asked", []needle.VolumeId{2}, []uint32{2}, nil},
		{"one ec volume asked", []needle.VolumeId{8}, nil, []uint32{8}},
		// Both kinds answer to the same set of ids.
		{"both kinds asked", []needle.VolumeId{1, 8}, []uint32{1}, []uint32{8}},
		{"every volume asked", []needle.VolumeId{1, 2, 3, 8, 9}, []uint32{1, 2, 3}, []uint32{8, 9}},
		// An id nothing answers to must not read as asking for everything.
		{"a missing id", []needle.VolumeId{7}, nil, nil},
		{"a found and a missing id", []needle.VolumeId{2, 7}, []uint32{2}, nil},
	} {
		t.Run(tc.name, func(t *testing.T) {
			ids := make(map[needle.VolumeId]struct{}, len(tc.ids))
			for _, id := range tc.ids {
				ids[id] = struct{}{}
			}
			volumes, ecVolumes := listed(topo.ToTopologyInfo(VolumeFilter{VolumeIds: ids}))
			if !equalIds(volumes, tc.wantVolumes) {
				t.Errorf("listed volumes %v, want %v", volumes, tc.wantVolumes)
			}
			if !equalIds(ecVolumes, tc.wantEcVolumes) {
				t.Errorf("listed ec volumes %v, want %v", ecVolumes, tc.wantEcVolumes)
			}
		})
	}
}

func TestVolumeFilterRemoteStorageNameWildcards(t *testing.T) {
	topo := NewTopology("filter", nil, 32*1024*1024*1024, 5, false)
	dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
		GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "", map[string]uint32{"": 100})
	topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
		{Id: 11, Collection: "c", Size: 100, Version: 3, DiskId: 3, RemoteStorageName: "s3.backup"},
		{Id: 12, Collection: "c", Size: 100, Version: 3, DiskId: 3, RemoteStorageName: "s3.archive"},
		{Id: 13, Collection: "c", Size: 100, Version: 3, DiskId: 3, RemoteStorageName: "gcs.cold"},
		{Id: 14, Collection: "c", Size: 100, Version: 3, DiskId: 3},
	}, dn)

	for _, tc := range []struct {
		name string
		f    VolumeFilter
		want []uint32
	}{
		// A nil name asks for every volume, tiered or local alike.
		{"no remote storage asked", VolumeFilter{}, []uint32{11, 12, 13, 14}},
		{"an exact name", VolumeFilter{remoteStorageName: new("s3.backup")}, []uint32{11}},
		{"a wildcarded storage", VolumeFilter{remoteStorageName: new("s3.*")}, []uint32{11, 12}},
		{"a wildcarded tier", VolumeFilter{remoteStorageName: new("*.cold")}, []uint32{13}},
		{"every tiered volume", VolumeFilter{remoteStorageName: new("*")}, []uint32{11, 12, 13}},
		// Not a wildcard, so it matches nothing rather than everything.
		{"a name no volume is on", VolumeFilter{remoteStorageName: new("s3.other")}, nil},
		// The local volumes are the ones with no remote storage name.
		{"the local ones only", VolumeFilter{remoteStorageName: new("")}, []uint32{14}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			volumes, ecVolumes := listed(topo.ToTopologyInfo(tc.f))
			if !equalIds(volumes, tc.want) {
				t.Errorf("listed volumes %v, want %v", volumes, tc.want)
			}
			if len(ecVolumes) != 0 {
				t.Errorf("listed ec volumes %v, want none", ecVolumes)
			}
		})
	}
}

// A filter never changes which disks are reported or what they say about
// themselves.
func TestVolumeFilterKeepsTheTopology(t *testing.T) {
	topo := filterTestTopology(t)
	full := topo.ToTopologyInfo(VolumeFilter{})
	name := "c"
	narrowed := topo.ToTopologyInfo(VolumeFilter{Collection: &name})

	for _, info := range []*master_pb.TopologyInfo{full, narrowed} {
		if len(info.DataCenterInfos) != 1 || len(info.DataCenterInfos[0].RackInfos) != 1 {
			t.Fatalf("listing lost its topology: %v", info)
		}
	}

	fullDisk := full.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
	narrowedDisk := narrowed.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
	if narrowedDisk == nil {
		t.Fatal("the filtered listing dropped the disk")
	}
	if narrowedDisk.VolumeCount != fullDisk.VolumeCount {
		t.Errorf("volume count %d, want the disk's own %d", narrowedDisk.VolumeCount, fullDisk.VolumeCount)
	}
	if narrowedDisk.MaxVolumeCount != fullDisk.MaxVolumeCount {
		t.Errorf("max volume count %d, want %d", narrowedDisk.MaxVolumeCount, fullDisk.MaxVolumeCount)
	}
	if narrowedDisk.DiskId != fullDisk.DiskId {
		t.Errorf("disk id %d, want %d", narrowedDisk.DiskId, fullDisk.DiskId)
	}
}

// So a caller that finds nothing can tell an empty answer from a missing disk.
func TestVolumeFilterKeepsTheDiskIdWhenNothingMatches(t *testing.T) {
	topo := filterTestTopology(t)
	name := "none"
	info := topo.ToTopologyInfo(VolumeFilter{Collection: &name})
	disk := info.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
	if len(disk.VolumeInfos) != 0 {
		t.Fatalf("expected nothing listed, got %d volumes", len(disk.VolumeInfos))
	}
	if disk.DiskId != 2 {
		t.Errorf("reported disk id %d, want the one its volumes are on", disk.DiskId)
	}
}

func TestNewVolumeFilterReadsTheRequest(t *testing.T) {
	collectionOf := func(f VolumeFilter) string {
		if f.Collection == nil {
			return "<every>"
		}
		return *f.Collection
	}

	for _, tc := range []struct {
		name    string
		request *master_pb.VolumeListRequest
		want    string
	}{
		// A caller passing through its own "" is answered too much, not wrongly.
		{"an empty request", &master_pb.VolumeListRequest{}, "<every>"},
		{"an empty collection", &master_pb.VolumeListRequest{Collection: ""}, "<every>"},
		{"a named collection", &master_pb.VolumeListRequest{Collection: "c"}, "c"},
		{"the default collection", &master_pb.VolumeListRequest{DefaultCollectionOnly: true}, ""},
		// Contradictory, so the more specific of the two wins.
		{"both", &master_pb.VolumeListRequest{Collection: "c", DefaultCollectionOnly: true}, "c"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			filter, err := NewVolumeFilter(tc.request)
			if err != nil {
				t.Fatal(err)
			}
			if got := collectionOf(filter); got != tc.want {
				t.Errorf("selected collection %q, want %q", got, tc.want)
			}
		})
	}
}

func TestNewVolumeFilterReadsTheRemoteStorageRequest(t *testing.T) {
	remoteStorageOf := func(f VolumeFilter) string {
		if f.remoteStorageName == nil {
			return "<every>"
		}
		return *f.remoteStorageName
	}

	for _, tc := range []struct {
		name    string
		request *master_pb.VolumeListRequest
		want    string
	}{
		// A caller passing through its own "" is answered too much, not wrongly.
		{"an empty request", &master_pb.VolumeListRequest{}, "<every>"},
		{"an empty remote storage name", &master_pb.VolumeListRequest{RemoteStorageName: ""}, "<every>"},
		{"a named remote storage", &master_pb.VolumeListRequest{RemoteStorageName: "s3.backup"}, "s3.backup"},
		// A wildcard is the caller's pattern, carried across untouched.
		{"a wildcarded storage", &master_pb.VolumeListRequest{RemoteStorageName: "*"}, "*"},
		// The one storage (local) the empty string cannot name.
		{"the local volumes only", &master_pb.VolumeListRequest{LocalVolumeOnly: true}, ""},
		// Contradictory, so the more specific of the two wins.
		{"both", &master_pb.VolumeListRequest{RemoteStorageName: "s3.backup", LocalVolumeOnly: true}, "s3.backup"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			filter, err := NewVolumeFilter(tc.request)
			if err != nil {
				t.Fatal(err)
			}
			if got := remoteStorageOf(filter); got != tc.want {
				t.Errorf("selected remote storage %q, want %q", got, tc.want)
			}
		})
	}
}

func TestNewVolumeFilterReadsTheVolumeIds(t *testing.T) {
	idsOf := func(f VolumeFilter) []uint32 {
		ids := make([]uint32, 0, len(f.VolumeIds))
		for id := range f.VolumeIds {
			ids = append(ids, uint32(id))
		}
		return ids
	}

	for _, tc := range []struct {
		name    string
		request *master_pb.VolumeListRequest
		want    []uint32
	}{
		// No id asked of the request, so every volume is listed.
		{"an empty request", &master_pb.VolumeListRequest{}, nil},
		{"one id", &master_pb.VolumeListRequest{VolumeIds: []uint32{7}}, []uint32{7}},
		{"a list of ids", &master_pb.VolumeListRequest{VolumeIds: []uint32{2, 8}}, []uint32{2, 8}},
		// Asking twice for a volume selects it once.
		{"duplicates", &master_pb.VolumeListRequest{VolumeIds: []uint32{2, 2}}, []uint32{2}},
		// The list is taken literally: no volume answers to zero, so a zero in
		// it narrows the listing rather than widening it.
		{"a zero among the ids", &master_pb.VolumeListRequest{VolumeIds: []uint32{0}}, []uint32{0}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			filter, err := NewVolumeFilter(tc.request)
			if err != nil {
				t.Fatal(err)
			}
			if got := idsOf(filter); !equalIds(got, tc.want) {
				t.Errorf("selected volume ids %v, want %v", got, tc.want)
			}
		})
	}
}

// topology_only leaves out the ec shards too, not just the volumes; the disks
// and their counters stay.
func TestNewVolumeFilterReadsTopologyOnly(t *testing.T) {
	topo := filterTestTopology(t)
	filter, err := NewVolumeFilter(&master_pb.VolumeListRequest{TopologyOnly: true})
	if err != nil {
		t.Fatal(err)
	}
	info := topo.ToTopologyInfo(filter)
	volumes, ecVolumes := listed(info)
	if len(volumes) != 0 || len(ecVolumes) != 0 {
		t.Errorf("listed volumes %v and ec volumes %v, want neither", volumes, ecVolumes)
	}

	full := topo.ToTopologyInfo(VolumeFilter{})
	fullDisk := full.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
	disk := info.DataCenterInfos[0].RackInfos[0].DataNodeInfos[0].DiskInfos[""]
	if disk == nil {
		t.Fatal("the topology_only listing dropped the disk")
	}
	if disk.VolumeCount != fullDisk.VolumeCount || disk.MaxVolumeCount != fullDisk.MaxVolumeCount {
		t.Errorf("disk counters %d/%d, want the full listing's %d/%d",
			disk.VolumeCount, disk.MaxVolumeCount, fullDisk.VolumeCount, fullDisk.MaxVolumeCount)
	}
}

// Selecting volumes in a request that asks for none is a caller bug, refused
// rather than resolved either way in silence.
func TestNewVolumeFilterRefusesTopologyOnlyWithSelectors(t *testing.T) {
	for _, tc := range []struct {
		name    string
		request *master_pb.VolumeListRequest
	}{
		{"a collection", &master_pb.VolumeListRequest{TopologyOnly: true, Collection: "c"}},
		{"volume ids", &master_pb.VolumeListRequest{TopologyOnly: true, VolumeIds: []uint32{1}}},
		{"the default collection", &master_pb.VolumeListRequest{TopologyOnly: true, DefaultCollectionOnly: true}},
		{"a remote storage", &master_pb.VolumeListRequest{TopologyOnly: true, RemoteStorageName: "s3.backup"}},
		{"the local volumes", &master_pb.VolumeListRequest{TopologyOnly: true, LocalVolumeOnly: true}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if _, err := NewVolumeFilter(tc.request); err == nil {
				t.Error("expected the contradiction refused, got a filter")
			}
		})
	}
}

// Everything a listing reads off a disk must be read under its lock, including
// how much room to reserve, or a heartbeat writing the map races it.
func TestVolumeFilterListsWhileVolumesChange(t *testing.T) {
	topo := filterTestTopology(t)
	dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
		GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "", map[string]uint32{"": 100})

	stop := make(chan struct{})
	done := make(chan struct{})
	go func() {
		defer close(done)
		for i := 0; ; i++ {
			select {
			case <-stop:
				return
			default:
			}
			topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
				{Id: uint32(100 + i%50), Collection: "c", Size: 1, Version: 3, DiskId: 2},
			}, dn)
		}
	}()

	name := "c"
	for i := 0; i < 200; i++ {
		topo.ToTopologyInfo(VolumeFilter{})
		topo.ToTopologyInfo(VolumeFilter{Collection: &name})
	}
	close(stop)
	<-done
}
