package daemon

import (
	"context"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
	"testing"
	"time"

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

// TestPullImageDoesNotSerialiseOnDifferentNames confirms the refactor
// actually releases imageOpsMu during the slow staging phase: two
// PullImage calls for distinct names run concurrently, with the
// "pull" half overlapping in time. Before the fix the two would have
// run strictly sequentially (one blocking the other inside
// imageOpsMu across the full OCI pull), which the maxActive >= 2
// assertion would fail.
func TestPullImageDoesNotSerialiseOnDifferentNames(t *testing.T) {
	if _, err := os.Stat("/usr/bin/mkfs.ext4"); err != nil {
		if _, err := os.Stat("/sbin/mkfs.ext4"); err != nil {
			t.Skip("mkfs.ext4 not available; skipping")
		}
	}
	imagesDir := t.TempDir()
	cacheDir := t.TempDir()
	kernel, initrd, modules := writeFakeKernelTriple(t)

	var (
		active       atomic.Int32
		maxActive    atomic.Int32
		enterPull    = make(chan struct{})
		startRelease = make(chan struct{})
	)

	slowPullAndFlatten := func(_ context.Context, _ string, _ string, destDir string) (imagepull.Metadata, error) {
		// Record that we entered the pull body.
		enterPull <- struct{}{}
		// Track concurrent overlap.
		n := active.Add(1)
		for {
			cur := maxActive.Load()
			if n <= cur || maxActive.CompareAndSwap(cur, n) {
				break
			}
		}
		// Wait for the test to unblock us AFTER both pulls have
		// entered the body.
		<-startRelease
		active.Add(-1)
		// Produce the minimal synthetic tree stubPullAndFlatten does.
		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
		}
		return imagepull.Metadata{Entries: map[string]imagepull.FileMeta{}}, nil
	}

	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:       slowPullAndFlatten,
		finalizePulledRootfs: stubFinalizePulledRootfs,
		workSeedBuilder:      stubWorkSeedBuilder,
	}
	wireServices(d)

	mkParams := func(name string) api.ImagePullParams {
		return api.ImagePullParams{
			Ref:        "example.invalid/" + name + ":latest",
			Name:       name,
			KernelPath: kernel,
			InitrdPath: initrd,
			ModulesDir: modules,
		}
	}

	var wg sync.WaitGroup
	errs := make([]error, 2)
	for i, name := range []string{"alpha", "beta"} {
		wg.Add(1)
		go func(i int, name string) {
			defer wg.Done()
			_, err := d.img.PullImage(context.Background(), mkParams(name))
			errs[i] = err
		}(i, name)
	}

	// Wait for BOTH pulls to enter the slow body before we release
	// them. If imageOpsMu still wrapped the full flow, the second
	// pull would block on the mutex and never reach the enterPull
	// send — the timeout below would fire.
	for i := 0; i < 2; i++ {
		select {
		case <-enterPull:
		case <-time.After(3 * time.Second):
			t.Fatalf("pull %d never entered the slow body — imageOpsMu still serialises distinct pulls", i+1)
		}
	}
	close(startRelease)
	wg.Wait()

	for i, err := range errs {
		if err != nil {
			t.Fatalf("pull %d failed: %v", i+1, err)
		}
	}
	if maxActive.Load() < 2 {
		t.Fatalf("maxActive = %d, want >= 2 (pulls did not overlap)", maxActive.Load())
	}
}

// TestPullImageRejectsNameClashAtPublish confirms the publish-window
// recheck is what actually enforces name uniqueness now that the slow
// body runs unlocked. Two pulls race to the same name; one wins and
// the other errors.
func TestPullImageRejectsNameClashAtPublish(t *testing.T) {
	if _, err := os.Stat("/usr/bin/mkfs.ext4"); err != nil {
		if _, err := os.Stat("/sbin/mkfs.ext4"); err != nil {
			t.Skip("mkfs.ext4 not available; skipping")
		}
	}
	imagesDir := t.TempDir()
	cacheDir := t.TempDir()
	kernel, initrd, modules := writeFakeKernelTriple(t)

	release := make(chan struct{})
	synchronised := make(chan struct{}, 2)
	pullAndFlatten := func(_ context.Context, _ string, _ string, destDir string) (imagepull.Metadata, error) {
		synchronised <- struct{}{}
		<-release
		if err := os.MkdirAll(filepath.Join(destDir, "etc"), 0o755); err != nil {
			return imagepull.Metadata{}, err
		}
		if err := os.WriteFile(filepath.Join(destDir, "marker"), []byte("ok"), 0o644); err != nil {
			return imagepull.Metadata{}, err
		}
		return imagepull.Metadata{Entries: map[string]imagepull.FileMeta{}}, nil
	}

	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:       pullAndFlatten,
		finalizePulledRootfs: stubFinalizePulledRootfs,
		workSeedBuilder:      stubWorkSeedBuilder,
	}
	wireServices(d)

	params := api.ImagePullParams{
		Ref:        "example.invalid/contender:latest",
		Name:       "contender",
		KernelPath: kernel,
		InitrdPath: initrd,
		ModulesDir: modules,
	}

	var wg sync.WaitGroup
	errs := make([]error, 2)
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			_, err := d.img.PullImage(context.Background(), params)
			errs[i] = err
		}(i)
	}
	// Both workers must enter the pull body before either publishes.
	for i := 0; i < 2; i++ {
		select {
		case <-synchronised:
		case <-time.After(3 * time.Second):
			t.Fatalf("pull %d never entered the slow body", i+1)
		}
	}
	close(release)
	wg.Wait()

	wins, losses := 0, 0
	for _, err := range errs {
		if err == nil {
			wins++
		} else {
			losses++
		}
	}
	if wins != 1 || losses != 1 {
		t.Fatalf("wins=%d losses=%d, want exactly one of each (errs=%v)", wins, losses, errs)
	}
}
