package daemon

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"net"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"testing"
	"time"

	"banger/internal/api"
	"banger/internal/buildinfo"
	"banger/internal/model"
	"banger/internal/paths"
	"banger/internal/rpc"
	"banger/internal/system"
)

// TestAuthorizeConnRejectsNonUnixConn pins the type guard at the top
// of authorizeConn: SO_PEERCRED only makes sense on a unix socket, so
// anything else must be refused outright. net.Pipe gives us a
// connection that satisfies net.Conn but isn't a *net.UnixConn, which
// is exactly the shape we need to exercise the early-return.
func TestAuthorizeConnRejectsNonUnixConn(t *testing.T) {
	d := &Daemon{}
	pipeA, pipeB := net.Pipe()
	defer pipeA.Close()
	defer pipeB.Close()
	if err := d.authorizeConn(pipeA); err == nil {
		t.Fatal("authorizeConn(pipe) succeeded, want error")
	}
}

// TestAuthorizeConnAcceptsOwnerUIDOverUnixSocket pins the happy path:
// when the test process connects to a freshly bound unix socket as
// itself, the daemon's peer-cred check matches d.clientUID and lets
// the connection through.
func TestAuthorizeConnAcceptsOwnerUIDOverUnixSocket(t *testing.T) {
	dir := t.TempDir()
	sockPath := filepath.Join(dir, "test.sock")
	listener, err := net.Listen("unix", sockPath)
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	defer listener.Close()

	type result struct {
		err error
	}
	got := make(chan result, 1)
	go func() {
		conn, err := listener.Accept()
		if err != nil {
			got <- result{err: err}
			return
		}
		defer conn.Close()
		d := &Daemon{clientUID: os.Getuid()}
		got <- result{err: d.authorizeConn(conn)}
	}()

	client, err := net.Dial("unix", sockPath)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	defer client.Close()

	select {
	case r := <-got:
		if r.err != nil {
			t.Fatalf("authorizeConn(unix self) = %v, want nil", r.err)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("authorizeConn never returned")
	}
}

func TestRegisterImageRequiresKernel(t *testing.T) {
	rootfs := filepath.Join(t.TempDir(), "rootfs.ext4")
	if err := os.WriteFile(rootfs, []byte("rootfs"), 0o644); err != nil {
		t.Fatalf("write rootfs: %v", err)
	}
	d := &Daemon{store: openDaemonStore(t)}
	wireServices(d)

	_, err := d.img.RegisterImage(context.Background(), api.ImageRegisterParams{
		Name:       "missing-kernel",
		RootfsPath: rootfs,
	})
	if err == nil || !strings.Contains(err.Error(), "kernel path is required") {
		t.Fatalf("RegisterImage() error = %v", err)
	}
}

func TestDispatchPingIncludesBuildInfo(t *testing.T) {
	d := &Daemon{pid: 42}
	wireServices(d)

	resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "ping"})
	if !resp.OK {
		t.Fatalf("dispatch(ping) = %+v, want ok", resp)
	}

	var got api.PingResult
	if err := json.Unmarshal(resp.Result, &got); err != nil {
		t.Fatalf("Unmarshal(PingResult): %v", err)
	}

	info := buildinfo.Current()
	if got.Status != "ok" || got.PID != 42 {
		t.Fatalf("PingResult = %+v, want status/pid populated", got)
	}
	if got.Version != info.Version || got.Commit != info.Commit || got.BuiltAt != info.BuiltAt {
		t.Fatalf("PingResult build info = %+v, want %+v", got, info)
	}
}

func TestServeReturnsOnContextCancel(t *testing.T) {
	dir := t.TempDir()
	runtimeDir := filepath.Join(dir, "runtime")
	if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
		t.Fatalf("MkdirAll runtime: %v", err)
	}
	socketPath := filepath.Join(runtimeDir, "bangerd.sock")
	probe, err := net.Listen("unix", filepath.Join(runtimeDir, "probe.sock"))
	if err != nil {
		if errors.Is(err, syscall.EPERM) || strings.Contains(err.Error(), "operation not permitted") {
			t.Skipf("unix socket listen blocked in this environment: %v", err)
		}
		t.Fatalf("probe listen: %v", err)
	}
	_ = probe.Close()
	_ = os.Remove(filepath.Join(runtimeDir, "probe.sock"))
	d := &Daemon{
		layout: paths.Layout{
			RuntimeDir: runtimeDir,
			SocketPath: socketPath,
		},
		config: model.DaemonConfig{
			StatsPollInterval: time.Hour,
		},
		store:     openDaemonStore(t),
		runner:    system.NewRunner(),
		logger:    slog.New(slog.NewTextHandler(io.Discard, nil)),
		closing:   make(chan struct{}),
		clientUID: -1,
		clientGID: -1,
	}
	wireServices(d)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	serveErr := make(chan error, 1)
	go func() {
		serveErr <- d.Serve(ctx)
	}()

	deadline := time.Now().Add(2 * time.Second)
	for {
		if _, err := os.Stat(socketPath); err == nil {
			break
		}
		select {
		case err := <-serveErr:
			t.Fatalf("Serve() returned before socket was ready: %v", err)
		default:
		}
		if time.Now().After(deadline) {
			t.Fatalf("socket %s not created before deadline", socketPath)
		}
		time.Sleep(25 * time.Millisecond)
	}

	cancel()

	select {
	case err := <-serveErr:
		if err != nil {
			t.Fatalf("Serve() error = %v, want nil on context cancel", err)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("Serve() did not return after context cancel")
	}
}

func TestPromoteImageCopiesBootArtifactsIntoArtifactDir(t *testing.T) {
	dir := t.TempDir()
	rootfs := filepath.Join(dir, "rootfs.ext4")
	kernel := filepath.Join(dir, "vmlinux")
	initrd := filepath.Join(dir, "initrd.img")
	modulesDir := filepath.Join(dir, "modules")
	if err := os.MkdirAll(modulesDir, 0o755); err != nil {
		t.Fatalf("mkdir modules: %v", err)
	}
	for path, data := range map[string]string{
		rootfs:                              "rootfs",
		kernel:                              "kernel",
		initrd:                              "initrd",
		filepath.Join(modulesDir, "depmod"): "modules",
	} {
		if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
			t.Fatalf("write %s: %v", path, err)
		}
	}

	db := openDaemonStore(t)
	image := model.Image{
		ID:         "img-promote",
		Name:       "void",
		Managed:    false,
		RootfsPath: rootfs,
		KernelPath: kernel,
		InitrdPath: initrd,
		ModulesDir: modulesDir,
		CreatedAt:  model.Now(),
		UpdatedAt:  model.Now(),
	}
	if err := db.UpsertImage(context.Background(), image); err != nil {
		t.Fatalf("UpsertImage: %v", err)
	}

	imagesDir := filepath.Join(dir, "images")
	if err := os.MkdirAll(imagesDir, 0o755); err != nil {
		t.Fatalf("mkdir images dir: %v", err)
	}

	d := &Daemon{
		layout: paths.Layout{ImagesDir: imagesDir},
		store:  db,
		runner: system.NewRunner(),
	}
	wireServices(d)
	got, err := d.img.PromoteImage(context.Background(), image.Name)
	if err != nil {
		t.Fatalf("PromoteImage: %v", err)
	}
	if !got.Managed {
		t.Fatal("promoted image should be managed")
	}
	for _, path := range []string{got.RootfsPath, got.KernelPath, got.InitrdPath, got.ModulesDir} {
		if !strings.HasPrefix(path, got.ArtifactDir) {
			t.Fatalf("artifact path %q does not live under %q", path, got.ArtifactDir)
		}
		if _, err := os.Stat(path); err != nil {
			t.Fatalf("stat %s: %v", path, err)
		}
	}
}
