package db

import (
	"database/sql"
	"errors"
	"strings"
	"testing"
	"time"

	"crypto-miner-server/internal/models"
)

func insertBuild(t *testing.T, d *Database, b *models.BuildRecord) {
	t.Helper()
	if err := d.InsertBuild(b); err != nil {
		t.Fatal(err)
	}
}

func TestBuildCRUDAndList(t *testing.T) {
	d := openTestDB(t)

	created := time.Now().UTC().Truncate(time.Second)
	build := &models.BuildRecord{
		ID:          "build-001",
		WorkerName:  "rig1",
		ServerURL:   "http://localhost:8080",
		Wallet:      "48xyz",
		Threads:     4,
		FileSize:    1024,
		BundleSize:  2048,
		FilePath:    "/data/builds/build-001.zip",
		FileName:    "agent.zip",
		DownloadURL: "/api/v1/builds/build-001/download",
		Platform:    "windows",
		CreatedAt:   created,
		PoolHost:    "pool.example.com",
		PoolPort:    443,
		PoolTLS:     true,
		PoolPass:    "x",
	}
	insertBuild(t, d, build)

	got, err := d.GetBuild("build-001")
	if err != nil {
		t.Fatal(err)
	}
	if got.WorkerName != "rig1" || got.Platform != "windows" || got.BundleSize != 2048 {
		t.Fatalf("build mismatch: %+v", got)
	}
	if got.Pinned {
		t.Fatal("new build should not be pinned")
	}

	list, err := d.ListBuilds(10)
	if err != nil {
		t.Fatal(err)
	}
	if len(list) != 1 || list[0].ID != "build-001" {
		t.Fatalf("list builds: %+v", list)
	}
}

func TestBuildExtraFilesRoundTrip(t *testing.T) {
	d := openTestDB(t)
	build := &models.BuildRecord{
		ID: "build-extra", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: time.Now(),
		DownloadURL: "/api/v1/builds/build-extra/download",
		ExtraFiles: []models.BuildExtraFile{
			{FileName: "payload.enc", FilePath: "/data/payload.enc"},
			{FileName: "README.txt"},
		},
	}
	insertBuild(t, d, build)

	got, err := d.GetBuild("build-extra")
	if err != nil {
		t.Fatal(err)
	}
	if len(got.ExtraFiles) != 2 || got.ExtraFiles[0].FileName != "payload.enc" {
		t.Fatalf("extra_files mismatch: %+v", got.ExtraFiles)
	}
}

func TestGetBuildNotFound(t *testing.T) {
	d := openTestDB(t)
	_, err := d.GetBuild("missing")
	if !errors.Is(err, sql.ErrNoRows) {
		t.Fatalf("expected sql.ErrNoRows, got %v", err)
	}
}

func TestSetPinnedBuild(t *testing.T) {
	d := openTestDB(t)
	now := time.Now()
	insertBuild(t, d, &models.BuildRecord{ID: "b1", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: now})
	insertBuild(t, d, &models.BuildRecord{ID: "b2", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: now.Add(time.Second)})

	if err := d.SetPinnedBuild("b1"); err != nil {
		t.Fatal(err)
	}
	b1, _ := d.GetBuild("b1")
	b2, _ := d.GetBuild("b2")
	if !b1.Pinned || b2.Pinned {
		t.Fatalf("pin state wrong: b1=%v b2=%v", b1.Pinned, b2.Pinned)
	}

	if err := d.SetPinnedBuild(""); err != nil {
		t.Fatal(err)
	}
	b1, _ = d.GetBuild("b1")
	if b1.Pinned {
		t.Fatal("empty id should unpin all builds")
	}
}

func TestSetPinnedBuildUnknownID(t *testing.T) {
	d := openTestDB(t)
	now := time.Now()
	insertBuild(t, d, &models.BuildRecord{ID: "b1", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: now, Pinned: true})

	err := d.SetPinnedBuild("missing")
	if err == nil {
		t.Fatal("expected error for unknown build id")
	}
	if !strings.Contains(err.Error(), "not found") {
		t.Fatalf("unexpected error: %v", err)
	}
	b1, _ := d.GetBuild("b1")
	if b1.Pinned {
		t.Fatal("unknown id should leave builds unpinned, not keep prior pin")
	}
}

func TestGetLatestBuildForPlatform(t *testing.T) {
	d := openTestDB(t)
	base := time.Now().UTC().Truncate(time.Second)

	insertBuild(t, d, &models.BuildRecord{
		ID: "old-linux", WorkerName: "w", ServerURL: "u", Wallet: "w",
		Platform: "linux", CreatedAt: base,
	})
	insertBuild(t, d, &models.BuildRecord{
		ID: "new-linux", WorkerName: "w", ServerURL: "u", Wallet: "w",
		Platform: "linux", CreatedAt: base.Add(time.Minute),
	})
	insertBuild(t, d, &models.BuildRecord{
		ID: "new-windows", WorkerName: "w", ServerURL: "u", Wallet: "w",
		Platform: "windows", CreatedAt: base.Add(2 * time.Minute),
	})

	latestLinux, err := d.GetLatestBuildForPlatform("linux")
	if err != nil {
		t.Fatal(err)
	}
	if latestLinux.ID != "new-linux" {
		t.Fatalf("latest linux: got %q", latestLinux.ID)
	}

	if err := d.SetPinnedBuild("old-linux"); err != nil {
		t.Fatal(err)
	}
	pinnedLinux, err := d.GetLatestBuildForPlatform("linux")
	if err != nil {
		t.Fatal(err)
	}
	if pinnedLinux.ID != "old-linux" {
		t.Fatalf("pinned linux: got %q", pinnedLinux.ID)
	}

	anyLatest, err := d.GetLatestBuildForPlatform("any")
	if err != nil {
		t.Fatal(err)
	}
	if anyLatest.ID != "old-linux" {
		t.Fatalf("any with pinned: got %q want old-linux", anyLatest.ID)
	}

	if err := d.SetPinnedBuild(""); err != nil {
		t.Fatal(err)
	}
	emptyPlatform, err := d.GetLatestBuildForPlatform("")
	if err != nil {
		t.Fatal(err)
	}
	if emptyPlatform.ID != "new-windows" {
		t.Fatalf("empty platform latest: got %q", emptyPlatform.ID)
	}
}

func TestGetLatestBuildForPlatformEmpty(t *testing.T) {
	d := openTestDB(t)
	_, err := d.GetLatestBuildForPlatform("linux")
	if !errors.Is(err, sql.ErrNoRows) {
		t.Fatalf("expected sql.ErrNoRows on empty db, got %v", err)
	}
}

func TestListBuildsOlderThanAndDeleteBuild(t *testing.T) {
	d := openTestDB(t)
	cutoff := time.Now().UTC().Truncate(time.Second)
	oldTime := cutoff.Add(-time.Hour)
	newTime := cutoff.Add(time.Hour)

	insertBuild(t, d, &models.BuildRecord{
		ID: "old-build", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: oldTime,
	})
	insertBuild(t, d, &models.BuildRecord{
		ID: "new-build", WorkerName: "w", ServerURL: "u", Wallet: "w", CreatedAt: newTime,
	})

	older, err := d.ListBuildsOlderThan(cutoff)
	if err != nil {
		t.Fatal(err)
	}
	if len(older) != 1 || older[0].ID != "old-build" {
		t.Fatalf("ListBuildsOlderThan: %+v", older)
	}

	if err := d.DeleteBuild("old-build"); err != nil {
		t.Fatal(err)
	}
	_, err = d.GetBuild("old-build")
	if !errors.Is(err, sql.ErrNoRows) {
		t.Fatalf("expected deleted build missing, got %v", err)
	}

	if err := d.DeleteBuild("never-existed"); err != nil {
		t.Fatal(err)
	}
}
