package metrics

import (
	"bytes"
	"context"
	"database/sql"
	"fmt"
	"net/http/httptest"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

type retentionTestClock struct {
	now time.Time
}

func (c *retentionTestClock) Now() time.Time {
	return c.now
}

func (c *retentionTestClock) Advance(d time.Duration) {
	c.now = c.now.Add(d)
}

func TestTenantSeriesTTLRemovesAllInstrumentKindsTogether(t *testing.T) {
	clock := &retentionTestClock{now: time.Unix(1_000, 0)}
	registry := NewRegistry()
	registry.configureTenantSeriesRetention(clock.Now)
	meter := NewMeterProvider(registry).Meter("retention_test")
	counter := meter.Int64Counter("retention_test_counter_total", "test counter")
	histogram := meter.Float64Histogram("retention_test_duration_seconds", "test histogram", []float64{0.1, 1})
	gauge := meter.Float64Gauge("retention_test_gauge", "test gauge")
	attrs := []Attribute{Attr("tenant_id", "tenant-expire"), Attr("tidbcloud_org_id", "org-expire")}

	counter.Add(1, attrs...)
	histogram.Observe(0.5, attrs...)
	gauge.Set(7, attrs...)

	var before bytes.Buffer
	registry.WritePrometheus(&before)
	if !strings.Contains(before.String(), `tenant_id="tenant-expire"`) {
		t.Fatalf("tenant series missing before expiry:\n%s", before.String())
	}

	clock.Advance(15 * time.Minute)
	var after bytes.Buffer
	registry.WritePrometheus(&after)
	text := after.String()
	if strings.Contains(text, `tenant_id="tenant-expire"`) {
		t.Fatalf("tenant series remained after TTL:\n%s", text)
	}
	if strings.Contains(text, "drive9_metrics_active_tenants") {
		t.Fatalf("active tenant self-metric should not be exported after TTL:\n%s", text)
	}
	if !strings.Contains(text, `drive9_metrics_tenant_evictions_total{reason="ttl"} 1`) {
		t.Fatalf("TTL eviction counter missing:\n%s", text)
	}
}

func TestTenantSeriesRetentionAdmitsNewTenantWithoutLimit(t *testing.T) {
	clock := &retentionTestClock{now: time.Unix(2_000, 0)}
	registry := NewRegistry()
	registry.configureTenantSeriesRetention(clock.Now)
	counter := NewMeterProvider(registry).Meter("retention_test").Int64Counter(
		"retention_unlimited_counter_total", "test counter",
	)

	counter.Add(1, Attr("tenant_id", "tenant-one"))
	counter.Add(1, Attr("tenant_id", "tenant-two"))
	counter.Add(2, Attr("tenant_id", "tenant-one"))

	var output bytes.Buffer
	registry.WritePrometheus(&output)
	text := output.String()
	if !strings.Contains(text, `retention_unlimited_counter_total{tenant_id="tenant-one"} 3`) {
		t.Fatalf("existing tenant did not remain writable:\n%s", text)
	}
	if !strings.Contains(text, `retention_unlimited_counter_total{tenant_id="tenant-two"} 1`) {
		t.Fatalf("new tenant was rejected despite unlimited retention:\n%s", text)
	}
	for _, selfMetric := range []string{
		"drive9_metrics_active_tenants",
		"drive9_metrics_tenant_updates_dropped_total",
	} {
		if strings.Contains(text, selfMetric) {
			t.Fatalf("obsolete retention self-metric %q was exported:\n%s", selfMetric, text)
		}
	}
}

func TestDeleteTenantSeriesAllowsReplacementTenant(t *testing.T) {
	clock := &retentionTestClock{now: time.Unix(3_000, 0)}
	registry := NewRegistry()
	registry.configureTenantSeriesRetention(clock.Now)
	counter := NewMeterProvider(registry).Meter("retention_test").Int64Counter(
		"retention_delete_counter_total", "test counter",
	)

	counter.Add(1, Attr("tenant_id", "tenant-delete"))
	registry.DeleteTenantSeries("tenant-delete")
	counter.Add(1, Attr("tenant_id", "tenant-replacement"))

	var output bytes.Buffer
	registry.WritePrometheus(&output)
	text := output.String()
	if strings.Contains(text, `tenant_id="tenant-delete"`) {
		t.Fatalf("deleted tenant remained exported:\n%s", text)
	}
	if !strings.Contains(text, `retention_delete_counter_total{tenant_id="tenant-replacement"} 1`) {
		t.Fatalf("replacement tenant was not recreated:\n%s", text)
	}
	if !strings.Contains(text, `drive9_metrics_tenant_evictions_total{reason="deleted"} 1`) {
		t.Fatalf("deleted eviction counter missing:\n%s", text)
	}
}

func TestTrackedTenantSnapshotDoesNotRefreshOrRecreateTenant(t *testing.T) {
	// Background maintenance snapshots are intentionally not tenant activity:
	// otherwise a five-minute observer would keep the tenant's entire metric
	// set alive forever and defeat the 15-minute inactivity TTL.
	clock := &retentionTestClock{now: time.Unix(4_000, 0)}
	registry := NewRegistry()
	registry.configureTenantSeriesRetention(clock.Now)
	meter := NewMeterProvider(registry).Meter("retention_test")
	counter := meter.Int64Counter("retention_activity_counter_total", "test counter")
	snapshot := meter.Float64Gauge("retention_snapshot_gauge", "test snapshot")
	attrs := []Attribute{Attr("tenant_id", "tenant-snapshot"), Attr("tidbcloud_org_id", "org-snapshot")}

	counter.Add(1, attrs...)
	clock.Advance(14 * time.Minute)
	snapshot.setForExistingTenant("tenant-snapshot", 7, attrs...)
	clock.Advance(time.Minute)

	var expired bytes.Buffer
	registry.WritePrometheus(&expired)
	if strings.Contains(expired.String(), `tenant_id="tenant-snapshot"`) {
		t.Fatalf("snapshot update extended tenant lifetime:\n%s", expired.String())
	}

	snapshot.setForExistingTenant("tenant-snapshot", 8, attrs...)
	var recreated bytes.Buffer
	registry.WritePrometheus(&recreated)
	if strings.Contains(recreated.String(), `tenant_id="tenant-snapshot"`) {
		t.Fatalf("snapshot update recreated expired tenant:\n%s", recreated.String())
	}
}

func TestTenantActivityRefreshesAllSeriesLifetime(t *testing.T) {
	clock := &retentionTestClock{now: time.Unix(4_500, 0)}
	registry := NewRegistry()
	registry.configureTenantSeriesRetention(clock.Now)
	meter := NewMeterProvider(registry).Meter("retention_test")
	counter := meter.Int64Counter("retention_refresh_counter_total", "test counter")
	gauge := meter.Float64Gauge("retention_refresh_gauge", "test gauge")
	attrs := []Attribute{Attr("tenant_id", "tenant-refresh"), Attr("tidbcloud_org_id", "org-refresh")}

	counter.Add(1, attrs...)
	gauge.Set(7, attrs...)
	clock.Advance(10 * time.Minute)
	counter.Add(1, attrs...)
	clock.Advance(6 * time.Minute)

	var active bytes.Buffer
	registry.WritePrometheus(&active)
	if !strings.Contains(active.String(), `retention_refresh_gauge{tenant_id="tenant-refresh",tidbcloud_org_id="org-refresh"} 7.000000`) {
		t.Fatalf("activity did not preserve all tenant series:\n%s", active.String())
	}

	clock.Advance(9 * time.Minute)
	var expired bytes.Buffer
	registry.WritePrometheus(&expired)
	if strings.Contains(expired.String(), `tenant_id="tenant-refresh"`) {
		t.Fatalf("tenant series remained 15 minutes after refreshed activity:\n%s", expired.String())
	}
}

func TestDeletingInstrumentSeriesRemovesTenantIndexReference(t *testing.T) {
	registry := NewRegistry()
	meter := NewMeterProvider(registry).Meter("retention_test")
	counter := meter.Int64Counter("retention_delete_ref_counter_total", "test counter")
	gauge := meter.Float64Gauge("retention_delete_ref_gauge", "test gauge")
	attrs := []Attribute{Attr("tenant_id", "tenant-delete-ref"), Attr("state", "active")}

	counter.Add(1, attrs...)
	gauge.Set(1, attrs...)
	counter.Delete(attrs...)
	gauge.Delete(attrs...)

	registry.mu.RLock()
	defer registry.mu.RUnlock()
	state := registry.tenantRetention.tenants["tenant-delete-ref"]
	if state == nil {
		t.Fatal("tenant state disappeared when only its series were deleted")
	}
	if len(state.refs) != 0 {
		t.Fatalf("deleted series left %d tenant index references", len(state.refs))
	}
}

func TestDeleteByTenantLabelRemovesTenantIndexReferences(t *testing.T) {
	registry := NewRegistry()
	meter := NewMeterProvider(registry).Meter("retention_test")
	counter := meter.Int64Counter("retention_delete_by_label_counter_total", "test counter")
	histogram := meter.Float64Histogram("retention_delete_by_label_duration_seconds", "test histogram", []float64{0.1, 1})
	gauge := meter.Float64Gauge("retention_delete_by_label_gauge", "test gauge")
	attrs := []Attribute{Attr("tenant_id", "tenant-delete-by-label"), Attr("state", "active")}

	counter.Add(1, attrs...)
	histogram.Observe(0.5, attrs...)
	gauge.Set(1, attrs...)
	registry.DeleteCountersByLabel("tenant_id", "tenant-delete-by-label")
	registry.DeleteHistogramsByLabel("tenant_id", "tenant-delete-by-label")
	registry.DeleteGaugesByLabel("tenant_id", "tenant-delete-by-label")

	registry.mu.RLock()
	defer registry.mu.RUnlock()
	state := registry.tenantRetention.tenants["tenant-delete-by-label"]
	if state == nil {
		t.Fatal("tenant state disappeared when only its series were deleted")
	}
	if len(state.refs) != 0 {
		t.Fatalf("delete-by-label left %d tenant index references", len(state.refs))
	}
}

func TestTenantSeriesRetentionConcurrentRecordScrapeAndDelete(t *testing.T) {
	registry := NewRegistry()
	meter := NewMeterProvider(registry).Meter("retention_test")
	counter := meter.Int64Counter("retention_concurrent_counter_total", "test counter")
	histogram := meter.Float64Histogram("retention_concurrent_duration_seconds", "test histogram", []float64{0.1, 1})
	gauge := meter.Float64Gauge("retention_concurrent_gauge", "test gauge")

	const iterations = 250
	var workers sync.WaitGroup
	workers.Add(3)
	go func() {
		defer workers.Done()
		for i := 0; i < iterations; i++ {
			attrs := []Attribute{Attr("tenant_id", fmt.Sprintf("tenant-%d", i%8)), Attr("state", "active")}
			counter.Add(1, attrs...)
			histogram.Observe(float64(i%10)/10, attrs...)
			gauge.Set(float64(i), attrs...)
		}
	}()
	go func() {
		defer workers.Done()
		for i := 0; i < iterations; i++ {
			var output bytes.Buffer
			registry.WritePrometheus(&output)
		}
	}()
	go func() {
		defer workers.Done()
		for i := 0; i < iterations; i++ {
			registry.DeleteTenantSeries(fmt.Sprintf("tenant-%d", i%8))
		}
	}()
	workers.Wait()

	for i := 0; i < 8; i++ {
		registry.DeleteTenantSeries(fmt.Sprintf("tenant-%d", i))
	}
	var output bytes.Buffer
	registry.WritePrometheus(&output)
	if strings.Contains(output.String(), `tenant_id="tenant-`) {
		t.Fatalf("tenant series remained after final deletes:\n%s", output.String())
	}
}

func TestTenantQuotaSnapshotExpiresWithTenantSeries(t *testing.T) {
	clock := &retentionTestClock{now: time.Unix(5_000, 0)}
	globalRegistry.mu.RLock()
	previousNow := globalRegistry.tenantRetention.now
	globalRegistry.mu.RUnlock()
	globalRegistry.configureTenantSeriesRetention(clock.Now)
	const tenantID = "tenant-quota-expire"
	const orgID = "org-quota-expire"
	DeleteTenantCounters(tenantID)
	t.Cleanup(func() {
		DeleteTenantCounters(tenantID)
		globalRegistry.configureTenantSeriesRetention(previousNow)
	})

	ReplaceTenantQuotaSnapshotByStorageTierWithOrg(tenantID, orgID, 10, nil, nil, false, 1, 100, true)
	clock.Advance(15 * time.Minute)

	var output bytes.Buffer
	globalRegistry.WritePrometheus(&output)
	text := output.String()
	if hasMetricLineWith(text, "drive9_tenant_storage_bytes", `tenant_id="`+tenantID+`"`) {
		t.Fatalf("expired storage quota metric remained exported:\n%s", text)
	}
	globalRegistry.mu.RLock()
	_, snapshotRemained := globalRegistry.tenantQuotaSnapshots[tenantID]
	globalRegistry.mu.RUnlock()
	if snapshotRemained {
		t.Fatal("tenant quota snapshot state remained after TTL")
	}
}

func TestTenantDBMetricsExpireWithTenantSeries(t *testing.T) {
	clock := &retentionTestClock{now: time.Unix(6_000, 0)}
	globalRegistry.mu.RLock()
	previousNow := globalRegistry.tenantRetention.now
	globalRegistry.mu.RUnlock()
	globalRegistry.configureTenantSeriesRetention(clock.Now)
	const tenantID = "tenant-db-expire"
	healthy := &atomic.Bool{}
	healthy.Store(true)
	db := sql.OpenDB(fakeConnector{healthy: healthy})
	db.SetMaxOpenConns(1)
	db.SetMaxIdleConns(1)
	RegisterTenantDB("user", tenantID, db)
	if err := db.PingContext(context.Background()); err != nil {
		t.Fatalf("ping tenant db: %v", err)
	}
	t.Cleanup(func() {
		UnregisterDB(db)
		_ = db.Close()
		DeleteTenantCounters(tenantID)
		globalRegistry.configureTenantSeriesRetention(previousNow)
	})

	before := httptest.NewRecorder()
	writeDBPrometheus(before)
	if !strings.Contains(before.Body.String(), `drive9_db_pool_registered{role="user",tenant_id="tenant-db-expire",tidbcloud_org_id="guest"} 1`) {
		t.Fatalf("tenant DB metric missing before expiry:\n%s", before.Body.String())
	}
	if !strings.Contains(before.Body.String(), `drive9_db_pool_registered{role="user"} 1`) {
		t.Fatalf("role-level registered DB metric missing before expiry:\n%s", before.Body.String())
	}
	if !strings.Contains(before.Body.String(), `drive9_db_pool_connections{role="user",state="open"} 1`) {
		t.Fatalf("role-level open DB metric missing before expiry:\n%s", before.Body.String())
	}

	clock.Advance(15 * time.Minute)
	after := httptest.NewRecorder()
	writeDBPrometheus(after)
	if strings.Contains(after.Body.String(), `tenant_id="tenant-db-expire"`) {
		t.Fatalf("tenant DB metric remained after TTL:\n%s", after.Body.String())
	}
	if !strings.Contains(after.Body.String(), `drive9_db_pool_registered{role="user"} 1`) {
		t.Fatalf("role-level registered DB metric disappeared with tenant series:\n%s", after.Body.String())
	}
	if !strings.Contains(after.Body.String(), `drive9_db_pool_connections{role="user",state="open"} 1`) {
		t.Fatalf("role-level open DB metric disappeared with tenant series:\n%s", after.Body.String())
	}
	if !strings.Contains(after.Body.String(), `drive9_db_pool_connections{role="user",state="in_use"} 0`) {
		t.Fatalf("tenant expiry removed the bounded role-level DB aggregate:\n%s", after.Body.String())
	}
}
