package daemon

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"banger/internal/api"
	"banger/internal/imagecat"
	"banger/internal/imagepull"
	"banger/internal/kernelcat"
	"banger/internal/model"
	"banger/internal/paths"
	"banger/internal/system"
)

// stubBundleFetch writes a valid-enough rootfs.ext4 + manifest.json
// into destDir, simulating a successful bundle download + extract.
// The returned manifest echoes the entry's declared kernel_ref so the
// orchestration sees the same hints it would from a real fetch.
func stubBundleFetch(manifest imagecat.Manifest) func(context.Context, string, imagecat.CatEntry) (imagecat.Manifest, error) {
	return func(_ context.Context, destDir string, entry imagecat.CatEntry) (imagecat.Manifest, error) {
		if err := os.WriteFile(filepath.Join(destDir, imagecat.RootfsFilename), []byte("rootfs-bytes"), 0o644); err != nil {
			return imagecat.Manifest{}, err
		}
		m := manifest
		if m.Name == "" {
			m.Name = entry.Name
		}
		data, err := json.Marshal(m)
		if err != nil {
			return imagecat.Manifest{}, err
		}
		if err := os.WriteFile(filepath.Join(destDir, imagecat.ManifestFilename), data, 0o644); err != nil {
			return imagecat.Manifest{}, err
		}
		return m, nil
	}
}

func seedKernel(t *testing.T, kernelsDir, name string) {
	t.Helper()
	if err := kernelcat.WriteLocal(kernelsDir, kernelcat.Entry{
		Name:   name,
		Distro: "generic",
		Arch:   "x86_64",
		Source: "test",
	}); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(kernelsDir, name, "vmlinux"), []byte("kernel"), 0o644); err != nil {
		t.Fatal(err)
	}
}

func TestPullImageBundlePathRegistersFromCatalog(t *testing.T) {
	imagesDir := t.TempDir()
	kernelsDir := t.TempDir()
	seedKernel(t, kernelsDir, "generic-6.12")

	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout:          d.layout,
		store:           d.store,
		runner:          d.runner,
		bundleFetch:     stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
		workSeedBuilder: stubWorkSeedBuilder,
	}
	wireServices(d)

	entry := imagecat.CatEntry{
		Name:          "debian-bookworm",
		Distro:        "debian",
		Arch:          "x86_64",
		KernelRef:     "generic-6.12",
		TarballURL:    "https://example.com/x.tar.zst",
		TarballSHA256: "abc",
	}
	image, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "debian-bookworm"}, entry)
	if err != nil {
		t.Fatalf("pullFromBundle: %v", err)
	}
	if image.Name != "debian-bookworm" {
		t.Errorf("Name = %q, want debian-bookworm", image.Name)
	}
	if !strings.HasPrefix(image.ArtifactDir, imagesDir) {
		t.Errorf("ArtifactDir = %q, want under %q", image.ArtifactDir, imagesDir)
	}
	for _, rel := range []string{"rootfs.ext4", "kernel"} {
		if _, err := os.Stat(filepath.Join(image.ArtifactDir, rel)); err != nil {
			t.Errorf("missing artifact %s: %v", rel, err)
		}
	}
	// manifest.json should not leak into the published artifact dir.
	if _, err := os.Stat(filepath.Join(image.ArtifactDir, imagecat.ManifestFilename)); !os.IsNotExist(err) {
		t.Errorf("manifest.json should be stripped, got err=%v", err)
	}
}

func TestPullImageBundlePathOverrideNameAndKernelRef(t *testing.T) {
	imagesDir := t.TempDir()
	kernelsDir := t.TempDir()
	seedKernel(t, kernelsDir, "custom-kernel")
	// Overwrite the vmlinux with recognisable bytes so we can verify
	// the staged kernel came from the --kernel-ref entry, not the
	// catalog's kernel_ref.
	customBytes := []byte("custom-kernel-marker")
	if err := os.WriteFile(filepath.Join(kernelsDir, "custom-kernel", "vmlinux"), customBytes, 0o644); err != nil {
		t.Fatal(err)
	}

	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout:          d.layout,
		store:           d.store,
		runner:          d.runner,
		bundleFetch:     stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
		workSeedBuilder: stubWorkSeedBuilder,
	}
	wireServices(d)

	entry := imagecat.CatEntry{
		Name: "debian-bookworm", Arch: "x86_64",
		KernelRef:     "generic-6.12",
		TarballURL:    "https://example.com/x.tar.zst",
		TarballSHA256: "abc",
	}
	image, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{
		Ref: "debian-bookworm", Name: "my-sandbox", KernelRef: "custom-kernel",
	}, entry)
	if err != nil {
		t.Fatalf("pullFromBundle: %v", err)
	}
	if image.Name != "my-sandbox" {
		t.Errorf("Name = %q, want my-sandbox", image.Name)
	}
	staged, err := os.ReadFile(image.KernelPath)
	if err != nil {
		t.Fatalf("read staged kernel: %v", err)
	}
	if !strings.Contains(string(staged), "custom-kernel-marker") {
		t.Errorf("staged kernel = %q, want custom-kernel bytes", staged)
	}
}

func TestPullImageBundlePathRejectsExistingName(t *testing.T) {
	imagesDir := t.TempDir()
	kernelsDir := t.TempDir()
	seedKernel(t, kernelsDir, "generic-6.12")

	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout:          d.layout,
		store:           d.store,
		runner:          d.runner,
		bundleFetch:     stubBundleFetch(imagecat.Manifest{KernelRef: "generic-6.12"}),
		workSeedBuilder: stubWorkSeedBuilder,
	}
	wireServices(d)
	id, _ := model.NewID()
	if err := d.store.UpsertImage(context.Background(), model.Image{
		ID: id, Name: "debian-bookworm",
		CreatedAt: model.Now(), UpdatedAt: model.Now(),
	}); err != nil {
		t.Fatal(err)
	}

	_, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "debian-bookworm"}, imagecat.CatEntry{
		Name: "debian-bookworm", KernelRef: "generic-6.12",
		TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
	})
	if err == nil || !strings.Contains(err.Error(), "already exists") {
		t.Fatalf("expected already-exists, got %v", err)
	}
}

func TestPullImageBundlePathRequiresSomeKernelSource(t *testing.T) {
	d := &Daemon{
		layout: paths.Layout{ImagesDir: t.TempDir(), KernelsDir: t.TempDir()},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout:          d.layout,
		store:           d.store,
		runner:          d.runner,
		bundleFetch:     stubBundleFetch(imagecat.Manifest{}),
		workSeedBuilder: stubWorkSeedBuilder,
	}
	wireServices(d)
	// Catalog entry has no kernel_ref, no --kernel-ref/--kernel passed.
	_, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "x"}, imagecat.CatEntry{
		Name: "x", TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
	})
	if err == nil || !strings.Contains(err.Error(), "kernel") {
		t.Fatalf("expected kernel-required error, got %v", err)
	}
}

func TestPullImageBundleFetchFailurePropagates(t *testing.T) {
	imagesDir := t.TempDir()
	kernelsDir := t.TempDir()
	seedKernel(t, kernelsDir, "generic-6.12")

	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout: d.layout,
		store:  d.store,
		runner: d.runner,
		bundleFetch: func(_ context.Context, _ string, _ imagecat.CatEntry) (imagecat.Manifest, error) {
			return imagecat.Manifest{}, errors.New("r2 exploded")
		},
		workSeedBuilder: stubWorkSeedBuilder,
	}
	wireServices(d)
	_, err := d.img.pullFromBundle(context.Background(), api.ImagePullParams{Ref: "x"}, imagecat.CatEntry{
		Name: "x", KernelRef: "generic-6.12",
		TarballURL: "https://example.com/x.tar.zst", TarballSHA256: "abc",
	})
	if err == nil || !strings.Contains(err.Error(), "r2 exploded") {
		t.Fatalf("expected fetch failure propagated, got %v", err)
	}
	// Staging dir cleaned up.
	stagings, _ := filepath.Glob(filepath.Join(imagesDir, "*.staging"))
	if len(stagings) != 0 {
		t.Errorf("staging dirs left behind: %v", stagings)
	}
}

func TestPullImageDispatchFallsThroughToOCIWhenNoCatalogHit(t *testing.T) {
	imagesDir := t.TempDir()
	kernelsDir := t.TempDir()
	seedKernel(t, kernelsDir, "generic-6.12")

	ociCalled := false
	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir, KernelsDir: kernelsDir, OCICacheDir: t.TempDir()},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout: d.layout,
		store:  d.store,
		runner: d.runner,
		pullAndFlatten: func(_ context.Context, ref, _ string, destDir string) (imagepull.Metadata, error) {
			ociCalled = true
			if err := os.WriteFile(filepath.Join(destDir, "marker"), []byte("x"), 0o644); err != nil {
				return imagepull.Metadata{}, err
			}
			return imagepull.Metadata{}, errors.New("stop here")
		},
		finalizePulledRootfs: stubFinalizePulledRootfs,
		bundleFetch:          stubBundleFetch(imagecat.Manifest{}),
		workSeedBuilder:      stubWorkSeedBuilder,
	}
	wireServices(d)

	_, err := d.img.PullImage(context.Background(), api.ImagePullParams{
		// Not a catalog name (catalog is empty in the embedded default).
		Ref:       "docker.io/library/debian:bookworm",
		KernelRef: "generic-6.12",
	})
	if err == nil || !strings.Contains(err.Error(), "stop here") {
		t.Fatalf("expected OCI path to be taken, got %v", err)
	}
	if !ociCalled {
		t.Fatal("OCI seam was not invoked")
	}
}
