package metrics

import (
	"bytes"
	"strings"
	"sync"
	"testing"
	"time"
)

func TestObserveJobBuckets(t *testing.T) {
	r := NewRegistry()
	// Bucket boundaries: 1, 5, 30, 60, 300, 1800, 3600, 21600, 86400
	r.ObserveJob("backup", "succeeded", 500*time.Millisecond) // <= 1
	r.ObserveJob("backup", "succeeded", 30*time.Second)       // == 30 (boundary)
	r.ObserveJob("backup", "succeeded", 90*time.Second)       // > 60, <= 300
	r.ObserveJob("backup", "succeeded", 2*time.Hour)          // > 3600 → 21600 bucket
	rows := r.snapshotJobs()
	if len(rows) != 1 {
		t.Fatalf("rows: %d", len(rows))
	}
	row := rows[0]
	if row.Count != 4 {
		t.Errorf("count: %d", row.Count)
	}
	wantSum := 0.5 + 30 + 90 + 7200.0
	if row.Sum != wantSum {
		t.Errorf("sum: got %v want %v", row.Sum, wantSum)
	}
	// Cumulative buckets:
	//  le=1     → 1 (the 0.5s)
	//  le=5     → 1
	//  le=30    → 2 (boundary inclusive: 30s included)
	//  le=60    → 2
	//  le=300   → 3
	//  le=1800  → 3
	//  le=3600  → 3
	//  le=21600 → 4
	//  le=86400 → 4
	//  le=+Inf  → 4
	want := []uint64{1, 1, 2, 2, 3, 3, 3, 4, 4, 4}
	for i, w := range want {
		if row.Buckets[i] != w {
			t.Errorf("bucket[%d]=%d want %d", i, row.Buckets[i], w)
		}
	}
}

func TestObserveJobNegativeClampedToZero(t *testing.T) {
	r := NewRegistry()
	r.ObserveJob("backup", "succeeded", -5*time.Second)
	rows := r.snapshotJobs()
	if len(rows) != 1 || rows[0].Sum != 0 || rows[0].Count != 1 {
		t.Errorf("expected one zero-second observation, got %+v", rows)
	}
}

func TestObserveJobConcurrent(t *testing.T) {
	r := NewRegistry()
	const goroutines = 16
	const each = 200
	var wg sync.WaitGroup
	for g := 0; g < goroutines; g++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for i := 0; i < each; i++ {
				r.ObserveJob("backup", "succeeded", time.Second)
			}
		}()
	}
	wg.Wait()
	rows := r.snapshotJobs()
	if len(rows) != 1 {
		t.Fatalf("rows: %d", len(rows))
	}
	if rows[0].Count != uint64(goroutines*each) {
		t.Errorf("count: got %d want %d", rows[0].Count, goroutines*each)
	}
}

func TestObserveJobNilRegistryNoop(t *testing.T) {
	var r *Registry // nil
	r.ObserveJob("backup", "succeeded", time.Second)
}

func TestRenderGolden(t *testing.T) {
	r := NewRegistry()
	r.ObserveJob("backup", "succeeded", 5*time.Second)
	r.ObserveJob("forget", "succeeded", 100*time.Millisecond)

	pi64 := func(v int64) *int64 { return &v }
	pbool := func(v bool) *bool { return &v }

	hosts := []HostRow{
		{
			ID: "01H0001", Name: "alpha",
			Online:              true,
			LastBackupUnix:      pi64(1700000000),
			LastBackupSucceeded: pbool(true),
			RepoSizeBytes:       pi64(123456789),
			SnapshotCount:       42,
			OpenAlertCount:      0,
			RepoStatus:          "ready",
		},
		{
			ID: "01H0002", Name: "bravo",
			Online:         false,
			SnapshotCount:  0,
			OpenAlertCount: 1,
			RepoStatus:     "init_failed",
		},
	}
	snap := r.SnapshotWith(hosts,
		map[string]int{"info": 0, "warning": 1, "critical": 0},
		"v1.2.3", "deadbeef", "go1.25.0")

	var buf bytes.Buffer
	if err := Render(&buf, snap); err != nil {
		t.Fatalf("render: %v", err)
	}
	out := buf.String()

	for _, want := range []string{
		"# HELP rm_hosts_total ",
		"rm_hosts_total 2\n",
		"rm_hosts_online 1\n",
		`rm_active_alerts{severity="warning"} 1`,
		`rm_active_alerts{severity="info"} 0`,
		`rm_active_alerts{severity="critical"} 0`,
		`rm_build_info{version="v1.2.3",commit="deadbeef",go_version="go1.25.0"} 1`,
		`rm_host_agent_online{host_id="01H0001",host="alpha"} 1`,
		`rm_host_agent_online{host_id="01H0002",host="bravo"} 0`,
		`rm_host_last_backup_timestamp_seconds{host_id="01H0001",host="alpha"} 1700000000`,
		`rm_host_last_backup_success{host_id="01H0001",host="alpha"} 1`,
		`rm_host_repo_size_bytes{host_id="01H0001",host="alpha"} 123456789`,
		`rm_host_snapshot_count{host_id="01H0001",host="alpha"} 42`,
		`rm_host_snapshot_count{host_id="01H0002",host="bravo"} 0`,
		`rm_host_open_alerts{host_id="01H0002",host="bravo"} 1`,
		`rm_host_repo_status{host_id="01H0001",host="alpha",status="ready"} 1`,
		`rm_host_repo_status{host_id="01H0002",host="bravo",status="init_failed"} 1`,
		`rm_job_duration_seconds_bucket{kind="backup",status="succeeded",le="1"} 0`,
		`rm_job_duration_seconds_bucket{kind="backup",status="succeeded",le="5"} 1`,
		`rm_job_duration_seconds_bucket{kind="backup",status="succeeded",le="+Inf"} 1`,
		`rm_job_duration_seconds_sum{kind="backup",status="succeeded"} 5`,
		`rm_job_duration_seconds_count{kind="backup",status="succeeded"} 1`,
		`rm_job_duration_seconds_bucket{kind="forget",status="succeeded",le="1"} 1`,
	} {
		if !strings.Contains(out, want) {
			t.Errorf("missing line:\n  %s\n--- full output ---\n%s", want, out)
		}
	}

	// bravo had no last backup → those metric lines must be absent for it.
	for _, ban := range []string{
		`rm_host_last_backup_timestamp_seconds{host_id="01H0002"`,
		`rm_host_last_backup_success{host_id="01H0002"`,
		`rm_host_repo_size_bytes{host_id="01H0002"`,
	} {
		if strings.Contains(out, ban) {
			t.Errorf("unexpected line for bravo: %q", ban)
		}
	}
}

func TestRenderEmptySnapshot(t *testing.T) {
	r := NewRegistry()
	snap := r.SnapshotWith(nil, nil, "dev", "", "go1.25.0")
	var buf bytes.Buffer
	if err := Render(&buf, snap); err != nil {
		t.Fatalf("render: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "rm_hosts_total 0\n") {
		t.Errorf("missing zero-host gauge:\n%s", out)
	}
	// Histogram block has its HELP/TYPE but no rows. The HELP/TYPE
	// presence is correct and helps Prometheus pre-register the metric.
	if !strings.Contains(out, "# TYPE rm_job_duration_seconds histogram") {
		t.Errorf("histogram HELP/TYPE missing")
	}
}
