package daemon

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

	"banger/internal/api"
	"banger/internal/imagepull"
	"banger/internal/model"
	"banger/internal/paths"
	"banger/internal/system"

	"github.com/google/go-containerregistry/pkg/name"
)

func writeFakeKernelTriple(t *testing.T) (kernelPath, initrdPath, modulesDir string) {
	t.Helper()
	dir := t.TempDir()
	kernelPath = filepath.Join(dir, "vmlinux")
	if err := os.WriteFile(kernelPath, []byte("kernel"), 0o644); err != nil {
		t.Fatal(err)
	}
	initrdPath = filepath.Join(dir, "initrd.img")
	if err := os.WriteFile(initrdPath, []byte("initrd"), 0o644); err != nil {
		t.Fatal(err)
	}
	modulesDir = filepath.Join(dir, "modules")
	if err := os.MkdirAll(modulesDir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(modulesDir, "modules.dep"), []byte(""), 0o644); err != nil {
		t.Fatal(err)
	}
	return
}

// stubFinalizePulledRootfs is a no-op seam substitute that skips the real
// debugfs + vsock-agent-binary injection machinery during daemon tests.
func stubFinalizePulledRootfs(_ context.Context, _ string, _ imagepull.Metadata) error {
	return nil
}

// stubWorkSeedBuilder returns an error so runBuildWorkSeed treats
// the step as non-fatal and proceeds without a work-seed. Keeps tests
// off sudo mount without asserting on WorkSeedPath.
func stubWorkSeedBuilder(_ context.Context, _ string, _ string) error {
	return errWorkSeedBuilderStub
}

var errWorkSeedBuilderStub = errors.New("work-seed builder stubbed in tests")

// stubPullAndFlatten writes a fixed file tree into destDir, simulating a
// successful OCI pull without the network or tarball machinery.
func stubPullAndFlatten(_ context.Context, _ string, _ string, destDir string) (imagepull.Metadata, error) {
	if err := os.MkdirAll(filepath.Join(destDir, "etc"), 0o755); err != nil {
		return imagepull.Metadata{}, err
	}
	if err := os.WriteFile(filepath.Join(destDir, "etc", "hello"), []byte("world"), 0o644); err != nil {
		return imagepull.Metadata{}, err
	}
	if err := os.WriteFile(filepath.Join(destDir, "marker"), []byte("ok"), 0o644); err != nil {
		return imagepull.Metadata{}, err
	}
	// Tiny synthetic metadata — daemon-level tests exercise the seam
	// plumbing, not the ownership pass itself.
	return imagepull.Metadata{Entries: map[string]imagepull.FileMeta{}}, nil
}

func TestPullImageHappyPath(t *testing.T) {
	if _, err := exec.LookPath("mkfs.ext4"); err != nil {
		t.Skip("mkfs.ext4 not available; skipping")
	}
	imagesDir := t.TempDir()
	cacheDir := t.TempDir()
	kernel, initrd, modules := writeFakeKernelTriple(t)

	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: cacheDir},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout:               d.layout,
		store:                d.store,
		runner:               d.runner,
		pullAndFlatten:       stubPullAndFlatten,
		finalizePulledRootfs: stubFinalizePulledRootfs,
		workSeedBuilder:      stubWorkSeedBuilder,
	}
	wireServices(d)

	image, err := d.img.PullImage(context.Background(), api.ImagePullParams{
		Ref:        "docker.io/library/debian:bookworm",
		KernelPath: kernel,
		InitrdPath: initrd,
		ModulesDir: modules,
	})
	if err != nil {
		t.Fatalf("PullImage: %v", err)
	}

	if image.Name != "debian-bookworm" {
		t.Errorf("Name = %q, want debian-bookworm", image.Name)
	}
	if !image.Managed {
		t.Errorf("expected Managed=true")
	}
	if image.ArtifactDir == "" || !strings.HasPrefix(image.ArtifactDir, imagesDir) {
		t.Errorf("ArtifactDir = %q, want under %q", image.ArtifactDir, imagesDir)
	}

	for _, rel := range []string{"rootfs.ext4", "kernel", "initrd.img", "modules"} {
		if _, err := os.Stat(filepath.Join(image.ArtifactDir, rel)); err != nil {
			t.Errorf("missing artifact %s: %v", rel, err)
		}
	}

	// Staging dir should be gone after publish.
	stagings, _ := filepath.Glob(filepath.Join(imagesDir, "*.staging"))
	if len(stagings) != 0 {
		t.Errorf("staging dirs left behind: %v", stagings)
	}
}

func TestPullImageRejectsExistingName(t *testing.T) {
	imagesDir := t.TempDir()
	kernel, _, _ := writeFakeKernelTriple(t)

	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout:               d.layout,
		store:                d.store,
		runner:               d.runner,
		pullAndFlatten:       stubPullAndFlatten,
		finalizePulledRootfs: stubFinalizePulledRootfs,
		workSeedBuilder:      stubWorkSeedBuilder,
	}
	wireServices(d)
	// Seed a preexisting image with the would-be derived name.
	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.PullImage(context.Background(), api.ImagePullParams{
		Ref:        "docker.io/library/debian:bookworm",
		KernelPath: kernel,
	})
	if err == nil || !strings.Contains(err.Error(), "already exists") {
		t.Fatalf("expected already-exists error, got %v", err)
	}
}

func TestPullImageRequiresKernel(t *testing.T) {
	d := &Daemon{
		layout: paths.Layout{ImagesDir: t.TempDir(), OCICacheDir: t.TempDir()},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout:               d.layout,
		store:                d.store,
		runner:               d.runner,
		pullAndFlatten:       stubPullAndFlatten,
		finalizePulledRootfs: stubFinalizePulledRootfs,
		workSeedBuilder:      stubWorkSeedBuilder,
	}
	wireServices(d)
	_, err := d.img.PullImage(context.Background(), api.ImagePullParams{
		Ref: "docker.io/library/debian:bookworm",
	})
	if err == nil || !strings.Contains(err.Error(), "kernel") {
		t.Fatalf("expected kernel-required error, got %v", err)
	}
}

func TestPullImageCleansStagingOnFailure(t *testing.T) {
	imagesDir := t.TempDir()
	kernel, _, _ := writeFakeKernelTriple(t)
	failureSeam := func(_ context.Context, _ string, _ string, _ string) (imagepull.Metadata, error) {
		return imagepull.Metadata{}, errors.New("network borked")
	}

	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir, OCICacheDir: t.TempDir()},
		store:  openDaemonStore(t),
		runner: system.NewRunner(),
	}
	d.img = &ImageService{
		layout:               d.layout,
		store:                d.store,
		runner:               d.runner,
		pullAndFlatten:       failureSeam,
		finalizePulledRootfs: stubFinalizePulledRootfs,
		workSeedBuilder:      stubWorkSeedBuilder,
	}
	wireServices(d)
	_, err := d.img.PullImage(context.Background(), api.ImagePullParams{
		Ref:        "docker.io/library/debian:bookworm",
		KernelPath: kernel,
	})
	if err == nil || !strings.Contains(err.Error(), "network borked") {
		t.Fatalf("expected propagated pull error, got %v", err)
	}
	stagings, _ := filepath.Glob(filepath.Join(imagesDir, "*.staging"))
	if len(stagings) != 0 {
		t.Errorf("staging dir left behind on failure: %v", stagings)
	}
}

func TestDefaultImageNameFromRef(t *testing.T) {
	cases := []struct {
		in   string
		want string
	}{
		{"docker.io/library/debian:bookworm", "debian-bookworm"},
		{"alpine:3.20", "alpine-3-20"},
		{"docker.io/library/debian", "debian"},
		{"ghcr.io/some/org/my-image:v2.1", "my-image-v2-1"},
	}
	for _, tc := range cases {
		ref, err := name.ParseReference(tc.in)
		if err != nil {
			t.Fatalf("parse %s: %v", tc.in, err)
		}
		if got := defaultImageNameFromRef(ref); got != tc.want {
			t.Errorf("defaultImageNameFromRef(%s) = %q, want %q", tc.in, got, tc.want)
		}
	}
}
