package client

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

	"crypto-miner-agent/config"
)

// ─── helpers ─────────────────────────────────────────────────────────────────

func cfgWithGPU(wallet, poolHost string, port int) config.RuntimeConfig {
	b := config.GetBuiltinConfig()
	b.GPUEnabled = true
	b.RVNWallet = wallet
	b.RVNPoolHost = poolHost
	b.RVNPoolPort = port
	b.RVNPoolPass = "x"
	b.RVNPoolTLS = false
	return config.RuntimeConfig{BuiltinConfig: b}
}

// ─── newGPUMiner gate checks ──────────────────────────────────────────────────

func TestNewGPUMinerGPUDisabled(t *testing.T) {
	b := config.GetBuiltinConfig()
	b.GPUEnabled = false
	b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9"
	cfg := config.RuntimeConfig{BuiltinConfig: b}
	if g := newGPUMiner(cfg); g != nil {
		t.Fatal("expected nil when GPUEnabled=false")
	}
}

func TestNewGPUMinerNoWallet(t *testing.T) {
	b := config.GetBuiltinConfig()
	b.GPUEnabled = true
	b.RVNWallet = ""
	cfg := config.RuntimeConfig{BuiltinConfig: b}
	if g := newGPUMiner(cfg); g != nil {
		t.Fatal("expected nil when RVNWallet is empty")
	}
}

func TestNewGPUMinerNoGPUDetected(t *testing.T) {
	// On this machine there is no nvidia-smi / wmic GPU — should return nil.
	cfg := cfgWithGPU("RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9", "rvn.2miners.com", 6060)
	// detectGPU will find no GPU on a headless CI box or dev machine without GPU.
	g := newGPUMiner(cfg)
	if g != nil {
		// A GPU was actually detected — not a test failure, just note it.
		t.Logf("GPU detected: %s (vendor=%v) — skipping nil-check", g.info.Model, g.info.Vendor)
	}
	// Either way, no panic — detection ran without crashing.
}

// ─── itoa ────────────────────────────────────────────────────────────────────

func TestItoaZero(t *testing.T) {
	if got := itoa(0); got != "0" {
		t.Fatalf("itoa(0) = %q, want \"0\"", got)
	}
}

func TestItoaPositive(t *testing.T) {
	cases := map[int]string{1: "1", 9: "9", 42: "42", 6060: "6060", 65535: "65535"}
	for n, want := range cases {
		if got := itoa(n); got != want {
			t.Errorf("itoa(%d) = %q, want %q", n, got, want)
		}
	}
}

func TestItoaNegative(t *testing.T) {
	if got := itoa(-5); got != "-5" {
		t.Fatalf("itoa(-5) = %q, want \"-5\"", got)
	}
}

// ─── buildPoolURL ─────────────────────────────────────────────────────────────

func TestBuildPoolURLTCP(t *testing.T) {
	ep := rvnEndpoint{host: "rvn.2miners.com", port: 6060, tls: false}
	got := buildPoolURL(ep)
	want := "stratum+tcp://rvn.2miners.com:6060"
	if got != want {
		t.Fatalf("buildPoolURL(tcp) = %q, want %q", got, want)
	}
}

func TestBuildPoolURLTLS(t *testing.T) {
	ep := rvnEndpoint{host: "rvn.2miners.com", port: 16060, tls: true}
	got := buildPoolURL(ep)
	want := "stratum+ssl://rvn.2miners.com:16060"
	if got != want {
		t.Fatalf("buildPoolURL(tls) = %q, want %q", got, want)
	}
}

// ─── buildPoolList ────────────────────────────────────────────────────────────

func TestBuildPoolListPrimaryOnly(t *testing.T) {
	cfg := cfgWithGPU("RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9", "rvn.2miners.com", 6060)
	g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})}
	pools := g.buildPoolList()
	if len(pools) != 1 {
		t.Fatalf("expected 1 pool, got %d", len(pools))
	}
	if pools[0].host != "rvn.2miners.com" || pools[0].port != 6060 {
		t.Errorf("unexpected primary pool: %+v", pools[0])
	}
}

func TestBuildPoolListWithBackups(t *testing.T) {
	b := config.GetBuiltinConfig()
	b.GPUEnabled = true
	b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9"
	b.RVNPoolHost = "rvn.2miners.com"
	b.RVNPoolPort = 6060
	b.RVNBackupPools = []config.BackupPool{
		{Host: "ravenminer.com", Port: 3838},
		{Host: "herominersrvn.com", Port: 1133},
	}
	cfg := config.RuntimeConfig{BuiltinConfig: b}
	g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})}
	pools := g.buildPoolList()
	if len(pools) != 3 {
		t.Fatalf("expected 3 pools (1 primary + 2 backup), got %d", len(pools))
	}
	if pools[1].host != "ravenminer.com" {
		t.Errorf("backup[0].host = %q, want ravenminer.com", pools[1].host)
	}
}

func TestBuildPoolListSkipsInvalidBackup(t *testing.T) {
	b := config.GetBuiltinConfig()
	b.GPUEnabled = true
	b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9"
	b.RVNPoolHost = "rvn.2miners.com"
	b.RVNPoolPort = 6060
	b.RVNBackupPools = []config.BackupPool{
		{Host: "", Port: 0},       // invalid — no host or port
		{Host: "valid.pool", Port: 3333},
	}
	cfg := config.RuntimeConfig{BuiltinConfig: b}
	g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})}
	pools := g.buildPoolList()
	if len(pools) != 2 {
		t.Fatalf("expected 2 pools (primary + 1 valid backup), got %d", len(pools))
	}
}

// ─── avg ─────────────────────────────────────────────────────────────────────

func TestAvgEmpty(t *testing.T) {
	if v := avg(nil, 5); v != 0 {
		t.Fatalf("avg(nil,5) = %f, want 0", v)
	}
}

func TestAvgAll(t *testing.T) {
	samples := []float64{10, 20, 30}
	if got := avg(samples, 3); got != 20 {
		t.Fatalf("avg([10,20,30],3) = %f, want 20", got)
	}
}

func TestAvgLastN(t *testing.T) {
	samples := []float64{100, 10, 20, 30}
	// last 3 = [10,20,30] → avg=20
	if got := avg(samples, 3); got != 20 {
		t.Fatalf("avg(last 3) = %f, want 20", got)
	}
}

func TestAvgLastNExceedsLen(t *testing.T) {
	samples := []float64{5, 10}
	if got := avg(samples, 100); got != 7.5 {
		t.Fatalf("avg(last 100 of 2) = %f, want 7.5", got)
	}
}

// ─── apiPort ─────────────────────────────────────────────────────────────────

func TestAPIPortNVIDIA(t *testing.T) {
	g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorNVIDIA}}
	if p := g.apiPort(); p != 4067 {
		t.Fatalf("NVIDIA API port = %d, want 4067", p)
	}
}

func TestAPIPortAMD(t *testing.T) {
	g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorAMD}}
	if p := g.apiPort(); p != 4068 {
		t.Fatalf("AMD API port = %d, want 4068", p)
	}
}

// ─── fetchMinerStats (mock HTTP) ─────────────────────────────────────────────

func TestFetchMinerStatsTRex(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := trexSummary{
			Hashrate: 42500000,
			GPUs: []struct {
				Temperature int `json:"temperature"`
				GpuLoad     int `json:"gpu_load"`
			}{{Temperature: 65, GpuLoad: 88}},
		}
		_ = json.NewEncoder(w).Encode(resp)
	}))
	defer srv.Close()

	var port int
	fmt.Sscanf(srv.URL[len("http://127.0.0.1:"):], "%d", &port)

	hr, tempC, usage, err := fetchMinerStats(GPUVendorNVIDIA, port)
	if err != nil {
		t.Fatalf("fetchMinerStats: %v", err)
	}
	if hr != 42500000 {
		t.Errorf("hashrate = %f, want 42500000", hr)
	}
	if tempC == nil || *tempC != 65 {
		t.Errorf("tempC = %v, want 65", tempC)
	}
	if usage == nil || *usage != 88 {
		t.Errorf("usage = %v, want 88", usage)
	}
}

func TestFetchMinerStatsTRM(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := trmStatus{
			Algorithms: []struct {
				Name     string  `json:"algorithm"`
				TotalMHs float64 `json:"mhsh_total"`
			}{{Name: "kawpow", TotalMHs: 25.5}},
			GPUs: []struct {
				TempC int `json:"temp_c"`
				Fan   int `json:"fan_pct"`
			}{{TempC: 72, Fan: 60}},
		}
		_ = json.NewEncoder(w).Encode(resp)
	}))
	defer srv.Close()

	var port int
	fmt.Sscanf(srv.URL[len("http://127.0.0.1:"):], "%d", &port)

	hr, tempC, _, err := fetchMinerStats(GPUVendorAMD, port)
	if err != nil {
		t.Fatalf("fetchMinerStats TRM: %v", err)
	}
	// 25.5 MH/s → 25_500_000 H/s
	if hr != 25.5e6 {
		t.Errorf("hashrate = %f, want %f", hr, 25.5e6)
	}
	if tempC == nil || *tempC != 72 {
		t.Errorf("tempC = %v, want 72", tempC)
	}
}

func TestFetchMinerStatsConnRefused(t *testing.T) {
	_, _, _, err := fetchMinerStats(GPUVendorNVIDIA, 59999)
	if err == nil {
		t.Fatal("expected error when miner API is not running")
	}
}

// ─── extractZipFile ──────────────────────────────────────────────────────────

func makeSyntheticZip(t *testing.T, filename, content string) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	fw, err := zw.Create(filename)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := fw.Write([]byte(content)); err != nil {
		t.Fatal(err)
	}
	if err := zw.Close(); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func TestExtractZipFileHappyPath(t *testing.T) {
	data := makeSyntheticZip(t, "t-rex.exe", "fake-binary-content")
	dir := t.TempDir()
	if err := extractZipFile(data, dir, "t-rex.exe"); err != nil {
		t.Fatalf("extractZipFile: %v", err)
	}
	got, err := os.ReadFile(filepath.Join(dir, "t-rex.exe"))
	if err != nil {
		t.Fatalf("output file not created: %v", err)
	}
	if string(got) != "fake-binary-content" {
		t.Errorf("content = %q, want \"fake-binary-content\"", got)
	}
}

func TestExtractZipFileTargetInSubdir(t *testing.T) {
	// Zip contains "release/t-rex.exe" — extractor should find by Base name.
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	fw, _ := zw.Create("release/t-rex.exe")
	_, _ = fw.Write([]byte("nested"))
	_ = zw.Close()

	dir := t.TempDir()
	if err := extractZipFile(buf.Bytes(), dir, "t-rex.exe"); err != nil {
		t.Fatalf("extractZipFile (nested): %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, "t-rex.exe")); err != nil {
		t.Fatalf("output file missing: %v", err)
	}
}

func TestExtractZipFileMissingTarget(t *testing.T) {
	data := makeSyntheticZip(t, "other.exe", "data")
	dir := t.TempDir()
	// Should not error — just silently skip (caller checks with os.Stat).
	if err := extractZipFile(data, dir, "t-rex.exe"); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, "t-rex.exe")); !os.IsNotExist(err) {
		t.Fatal("target should not have been created")
	}
}

func TestExtractZipFileInvalidData(t *testing.T) {
	dir := t.TempDir()
	err := extractZipFile([]byte("not a zip"), dir, "t-rex.exe")
	if err == nil {
		t.Fatal("expected error for invalid zip data")
	}
}

// ─── spec / download URL format ───────────────────────────────────────────────

func TestSpecNVIDIA(t *testing.T) {
	g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorNVIDIA}}
	s := g.spec()
	if s.fileName != "t-rex.exe" {
		t.Errorf("NVIDIA spec.fileName = %q, want t-rex.exe", s.fileName)
	}
	if s.downloadURL == "" {
		t.Error("NVIDIA spec.downloadURL is empty")
	}
}

func TestSpecAMD(t *testing.T) {
	g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorAMD}}
	s := g.spec()
	if s.fileName != "teamredminer.exe" {
		t.Errorf("AMD spec.fileName = %q, want teamredminer.exe", s.fileName)
	}
	if s.downloadURL == "" {
		t.Error("AMD spec.downloadURL is empty")
	}
}

// ─── Stop() on a never-started miner ─────────────────────────────────────────

func TestGPUMinerStopBeforeStart(t *testing.T) {
	g := &GPUMiner{stopCh: make(chan struct{})}
	done := make(chan struct{})
	go func() {
		defer close(done)
		g.Stop()
	}()
	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("Stop() hung when called before Start()")
	}
}

// ─── Stats returns zero until polled ─────────────────────────────────────────

func TestGPUMinerStatsDefault(t *testing.T) {
	g := &GPUMiner{stopCh: make(chan struct{})}
	stats, active := g.Stats()
	if active {
		t.Error("active should be false before Start()")
	}
	if stats.Hashrate15s != 0 || stats.Hashrate1m != 0 {
		t.Errorf("unexpected non-zero default stats: %+v", stats)
	}
}
