package daemon

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"banger/internal/api"
	"banger/internal/paths"
)

// seedFakeOCICache drops a few fixed-size files that mimic an OCI
// layer cache layout (blobs/sha256/<hex>) so tests don't depend on
// real registry round-trips.
func seedFakeOCICache(t *testing.T, cacheDir string) (totalBytes int64, blobCount int) {
	t.Helper()
	blobsDir := filepath.Join(cacheDir, "blobs", "sha256")
	if err := os.MkdirAll(blobsDir, 0o755); err != nil {
		t.Fatalf("MkdirAll: %v", err)
	}
	for i, payload := range []string{"layer-a", "layer-b-bigger", "layer-c"} {
		name := strings.Repeat("ab", 32) // 64 hex chars stand-in
		path := filepath.Join(blobsDir, name+"-"+string(rune('0'+i)))
		if err := os.WriteFile(path, []byte(payload), 0o644); err != nil {
			t.Fatalf("write blob: %v", err)
		}
		totalBytes += int64(len(payload))
		blobCount++
	}
	return totalBytes, blobCount
}

func TestPruneOCICacheDryRunReportsSizeWithoutDeleting(t *testing.T) {
	cacheRoot := t.TempDir()
	cacheDir := filepath.Join(cacheRoot, "oci")
	wantBytes, wantBlobs := seedFakeOCICache(t, cacheDir)

	d := &Daemon{layout: paths.Layout{OCICacheDir: cacheDir}}
	wireServices(d)

	res, err := d.img.PruneOCICache(context.Background(), api.ImageCachePruneParams{DryRun: true})
	if err != nil {
		t.Fatalf("PruneOCICache: %v", err)
	}
	if res.BytesFreed != wantBytes {
		t.Fatalf("BytesFreed = %d, want %d", res.BytesFreed, wantBytes)
	}
	if res.BlobsFreed != wantBlobs {
		t.Fatalf("BlobsFreed = %d, want %d", res.BlobsFreed, wantBlobs)
	}
	if !res.DryRun {
		t.Error("result.DryRun = false, want true")
	}
	// Blobs must still exist.
	entries, _ := os.ReadDir(filepath.Join(cacheDir, "blobs", "sha256"))
	if len(entries) != wantBlobs {
		t.Fatalf("blobs dir: got %d entries, want %d (dry-run must not delete)", len(entries), wantBlobs)
	}
}

func TestPruneOCICacheRemovesAllBlobs(t *testing.T) {
	cacheRoot := t.TempDir()
	cacheDir := filepath.Join(cacheRoot, "oci")
	wantBytes, wantBlobs := seedFakeOCICache(t, cacheDir)

	d := &Daemon{layout: paths.Layout{OCICacheDir: cacheDir}}
	wireServices(d)

	res, err := d.img.PruneOCICache(context.Background(), api.ImageCachePruneParams{})
	if err != nil {
		t.Fatalf("PruneOCICache: %v", err)
	}
	if res.BytesFreed != wantBytes {
		t.Fatalf("BytesFreed = %d, want %d", res.BytesFreed, wantBytes)
	}
	if res.BlobsFreed != wantBlobs {
		t.Fatalf("BlobsFreed = %d, want %d", res.BlobsFreed, wantBlobs)
	}
	if res.DryRun {
		t.Error("result.DryRun = true on a real prune")
	}
	// Cache dir must exist (recreated empty) so the next pull has a
	// place to write blobs.
	info, err := os.Stat(cacheDir)
	if err != nil {
		t.Fatalf("cache dir gone after prune: %v", err)
	}
	if !info.IsDir() {
		t.Fatal("cache path is not a directory after prune")
	}
	// Blobs subdir is gone (the rename took everything aside; the
	// recreate left only the bare cache dir).
	if _, err := os.Stat(filepath.Join(cacheDir, "blobs")); !os.IsNotExist(err) {
		t.Fatalf("blobs dir survived prune: %v", err)
	}
	// Aside dirs must have been cleaned up too.
	roots, _ := os.ReadDir(cacheRoot)
	for _, e := range roots {
		if strings.Contains(e.Name(), ".pruning-") {
			t.Errorf("aside dir leaked: %s", e.Name())
		}
	}
}

// TestPruneOCICacheMissingDirIsZeroResult covers the fresh-install
// case: no OCI pulls have ever happened, so the cache dir doesn't
// exist. Prune must report zero, not error.
func TestPruneOCICacheMissingDirIsZeroResult(t *testing.T) {
	cacheRoot := t.TempDir()
	cacheDir := filepath.Join(cacheRoot, "oci")
	// Don't create cacheDir.

	d := &Daemon{layout: paths.Layout{OCICacheDir: cacheDir}}
	wireServices(d)

	res, err := d.img.PruneOCICache(context.Background(), api.ImageCachePruneParams{})
	if err != nil {
		t.Fatalf("PruneOCICache(missing): %v", err)
	}
	if res.BytesFreed != 0 || res.BlobsFreed != 0 {
		t.Fatalf("missing cache should be zero; got %+v", res)
	}
}
