package system

import (
	"context"
	"os"
	"path/filepath"
	"reflect"
	"testing"
	"time"
)

func TestNewSampler(t *testing.T) {
	s := NewSampler("/proc/stat", time.Second)
	if s == nil {
		t.Fatal("NewSampler returned nil")
	}
	if s.statPath != "/proc/stat" {
		t.Errorf("statPath = %q, want /proc/stat", s.statPath)
	}
	if s.interval != time.Second {
		t.Errorf("interval = %v, want 1s", s.interval)
	}
}

func TestSamplerSnapshotsRealProcStat(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	s := NewSampler("/proc/stat", 10*time.Millisecond)
	s.Start(ctx)
	// Wait for at least two samples so there is a delta to compute.
	time.Sleep(50 * time.Millisecond)

	snap := s.Snapshot()
	if snap == nil {
		t.Fatal("Snapshot returned nil (expected at least one sample)")
	}
	if len(snap) == 0 {
		t.Fatal("Snapshot returned empty (expected at least one core)")
	}
	for _, c := range snap {
		if c.UsagePct < 0 || c.UsagePct > 100 {
			t.Errorf("core %d: usage_pct = %f, want 0–100", c.Core, c.UsagePct)
		}
	}
}

func TestSamplerReturnsCopy(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	s := NewSampler("/proc/stat", 10*time.Millisecond)
	s.Start(ctx)
	time.Sleep(50 * time.Millisecond)

	snap1 := s.Snapshot()
	snap2 := s.Snapshot()

	// Both should be non-nil and deep-equal.
	if snap1 == nil || snap2 == nil {
		t.Fatal("Snapshot returned nil")
	}
	if !reflect.DeepEqual(snap1, snap2) {
		t.Error("two sequential snapshots should be deep-equal")
	}

	// Mutating the returned slice should not affect the sampler.
	if len(snap1) > 0 {
		snap1[0].UsagePct = 999
		if snap2[0].UsagePct == 999 {
			t.Error("mutating one snapshot affected the other — expected a copy")
		}
	}
}

func TestSamplerStartOnce(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	s := NewSampler("/proc/stat", 10*time.Millisecond)
	s.Start(ctx)
	s.Start(ctx) // second call must not panic or create a second goroutine
	time.Sleep(30 * time.Millisecond)

	if snap := s.Snapshot(); snap == nil {
		t.Fatal("Snapshot returned nil after Start")
	}
}

func TestSamplerContextCancelStopsGoroutine(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	s := NewSampler("/proc/stat", 10*time.Millisecond)
	s.Start(ctx)

	time.Sleep(30 * time.Millisecond)
	if snap := s.Snapshot(); snap == nil {
		t.Fatal("Snapshot returned nil before cancel")
	}

	cancel()
	// Give the goroutine time to exit. If it doesn't, the test will hang.
	time.Sleep(20 * time.Millisecond)
	// Snapshot should still return the last cached value (no panic).
	if snap := s.Snapshot(); snap == nil {
		t.Fatal("Snapshot returned nil after cancel (cache should persist)")
	}
}

func TestSamplerWithFakeStat(t *testing.T) {
	dir := t.TempDir()
	statPath := filepath.Join(dir, "stat")
	content := []byte("cpu  100 20 30 400 10 5 3 2 0 0\ncpu0 50 10 15 200 5 3 1 1 0 0\ncpu1 50 10 15 200 5 2 2 1 0 0\n")
	if err := os.WriteFile(statPath, content, 0o644); err != nil {
		t.Fatal(err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	s := NewSampler(statPath, 20*time.Millisecond)
	s.Start(ctx)
	time.Sleep(50 * time.Millisecond)

	snap := s.Snapshot()
	if snap == nil {
		t.Fatal("Snapshot returned nil")
	}
	// With the stat file not changing between reads, deltas are zero → 0% usage.
	for _, c := range snap {
		if c.UsagePct != 0 {
			t.Errorf("core %d: expected 0%% usage (static stat file), got %f", c.Core, c.UsagePct)
		}
	}
}
