package storage

import (
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/seaweedfs/seaweedfs/telemetry/proto"
)

// seedSamples gives a cluster one sample per listed day offset (0 is today).
// The sample's Ts is filled in per day offset.
func seedSamples(s *PrometheusStorage, id string, sample HistorySample, dayOffsets ...int) {
	s.mu.Lock()
	defer s.mu.Unlock()
	for _, offset := range dayOffsets {
		sample.Ts = time.Now().AddDate(0, 0, offset).Unix()
		s.histories[id] = append(s.histories[id], sample)
	}
}

func TestClusterSizeSeries(t *testing.T) {
	s := newPrometheusStorage(prometheus.NewRegistry())

	// Reported every day of the window.
	seedSamples(s, "daily", HistorySample{TotalDiskBytes: 300, VolumeServerCount: 3},
		-9, -8, -7, -6, -5, -4, -3, -2, -1, 0)
	// Stopped reporting two days ago: still active, so its size is held to
	// the right edge instead of dropping out of the stack.
	seedSamples(s, "lagging", HistorySample{TotalDiskBytes: 200, VolumeServerCount: 2},
		-8, -7, -6, -5, -4, -3, -2)
	// Stopped reporting past the active window: its own days only.
	seedSamples(s, "gone", HistorySample{TotalDiskBytes: 900, VolumeServerCount: 9},
		-14, -13, -12, -11, -10, -9, -8)
	// One day of history only: unconfirmed, so it stays out of the stack.
	seedSamples(s, "oneshot", HistorySample{TotalDiskBytes: 400, VolumeServerCount: 4}, -1)

	series := s.GetClusterSizeSeries(10, 0)
	if len(series.Dates) != 10 {
		t.Fatalf("dates = %v, want 10 days", series.Dates)
	}
	if series.ClusterCount != 3 {
		t.Fatalf("cluster_count = %d, want 3", series.ClusterCount)
	}

	byId := map[string]ClusterSeries{}
	for _, c := range series.Clusters {
		byId[c.ClusterId] = c
	}
	if got := byId["daily"].Disk; !equal(got, []uint64{300, 300, 300, 300, 300, 300, 300, 300, 300, 300}) {
		t.Errorf("daily = %v, want 300 every day", got)
	}
	if got := byId["lagging"].Disk; !equal(got, []uint64{0, 200, 200, 200, 200, 200, 200, 200, 200, 200}) {
		t.Errorf("lagging = %v, want its size carried to the right edge", got)
	}
	if got := byId["gone"].Disk; !equal(got, []uint64{900, 900, 0, 0, 0, 0, 0, 0, 0, 0}) {
		t.Errorf("gone = %v, want no capacity after its last report", got)
	}
	if _, ok := byId["oneshot"]; ok {
		t.Errorf("unconfirmed cluster in the stack: %+v", byId["oneshot"])
	}

	// Volume servers ride the same axis and the same hold-forward rule.
	if got := byId["daily"].Servers; !equal(got, []uint64{3, 3, 3, 3, 3, 3, 3, 3, 3, 3}) {
		t.Errorf("daily servers = %v, want 3 every day", got)
	}
	if got := byId["lagging"].Servers; !equal(got, []uint64{0, 2, 2, 2, 2, 2, 2, 2, 2, 2}) {
		t.Errorf("lagging servers = %v, want carried to the right edge", got)
	}
	if got := byId["gone"].Servers; !equal(got, []uint64{9, 9, 0, 0, 0, 0, 0, 0, 0, 0}) {
		t.Errorf("gone servers = %v, want none after its last report", got)
	}

	// The totals are the last day's stack height: daily + lagging, not gone.
	if series.TotalDisk != 500 {
		t.Errorf("total_disk = %d, want 500", series.TotalDisk)
	}
	if series.TotalServers != 5 {
		t.Errorf("total_servers = %d, want 5", series.TotalServers)
	}
	// Largest on the last day comes first so the stack reads top-down.
	if series.Clusters[0].ClusterId != "daily" {
		t.Errorf("order = %s first, want daily", series.Clusters[0].ClusterId)
	}

	// Clusters past the limit are summed into "other", per day.
	series = s.GetClusterSizeSeries(10, 1)
	if len(series.Clusters) != 1 || series.Clusters[0].ClusterId != "daily" {
		t.Fatalf("limited clusters = %+v, want just daily", series.Clusters)
	}
	if series.Other == nil || series.Other.Count != 2 {
		t.Fatalf("other = %+v, want 2 clusters", series.Other)
	}
	if !equal(series.Other.Disk, []uint64{900, 1100, 200, 200, 200, 200, 200, 200, 200, 200}) {
		t.Errorf("other = %v, want lagging+gone summed per day", series.Other.Disk)
	}
	if !equal(series.Other.Servers, []uint64{9, 11, 2, 2, 2, 2, 2, 2, 2, 2}) {
		t.Errorf("other servers = %v, want lagging+gone summed per day", series.Other.Servers)
	}
	if series.ClusterCount != 3 || series.TotalDisk != 500 || series.TotalServers != 5 {
		t.Errorf("limit changed totals: count=%d disk=%d servers=%d, want 3/500/5",
			series.ClusterCount, series.TotalDisk, series.TotalServers)
	}
}

// A report that lands after an earlier one on the same day replaces it, so the
// series shows one value per cluster per day.
func TestClusterSizeSeriesUsesLatestDailySample(t *testing.T) {
	s := newPrometheusStorage(prometheus.NewRegistry())

	data := &proto.TelemetryData{
		TopologyId:        "aaaaaaaa-0000-0000-0000-000000000001",
		Version:           "4.40",
		Os:                "linux/amd64",
		TotalDiskBytes:    proto.MinDiskBytes + 100,
		VolumeServerCount: 4,
	}
	if err := s.StoreTelemetry(data); err != nil {
		t.Fatal(err)
	}
	data.TotalDiskBytes = proto.MinDiskBytes + 700
	data.VolumeServerCount = 6
	if err := s.StoreTelemetry(data); err != nil {
		t.Fatal(err)
	}

	// Nothing was reported before today, so the axis is today alone rather
	// than the full 3 days padded out with zeros.
	series := s.GetClusterSizeSeries(3, 0)
	if len(series.Clusters) != 1 {
		t.Fatalf("clusters = %+v, want 1", series.Clusters)
	}
	if today := time.Now().UTC().Format("2006-01-02"); len(series.Dates) != 1 || series.Dates[0] != today {
		t.Errorf("dates = %v, want %s only", series.Dates, today)
	}
	if got := series.Clusters[0].Disk; !equal(got, []uint64{proto.MinDiskBytes + 700}) {
		t.Errorf("disk = %v, want today's latest sample only", got)
	}
	if got := series.Clusters[0].Servers; !equal(got, []uint64{6}) {
		t.Errorf("servers = %v, want today's latest sample only", got)
	}
	if series.TotalDisk != proto.MinDiskBytes+700 || series.TotalServers != 6 {
		t.Errorf("totals = %d disk / %d servers, want %d/6", series.TotalDisk, series.TotalServers, proto.MinDiskBytes+700)
	}
}

func equal(a, b []uint64) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
