package daemon

import (
	"context"
	"errors"
	"net"
	"reflect"
	"testing"

	"banger/internal/firecracker"
	"banger/internal/guestconfig"
	"banger/internal/model"
	"banger/internal/system"
	"banger/internal/vmdns"
)

type testCapability struct {
	name           string
	prepare        func(context.Context, *model.VMRecord, model.Image) error
	cleanup        func(context.Context, model.VMRecord) error
	contribute     func(*guestconfig.Builder, model.VMRecord, model.Image)
	contributeFC   func(*firecracker.MachineConfig, model.VMRecord, model.Image)
	configChange   func(context.Context, model.VMRecord, model.VMRecord) error
	doctor         func(context.Context, *system.Report)
	startPreflight func(context.Context, *system.Preflight, model.VMRecord, model.Image)
}

func (c testCapability) Name() string { return c.name }

func (c testCapability) PrepareHost(ctx context.Context, vm *model.VMRecord, image model.Image) error {
	if c.prepare != nil {
		return c.prepare(ctx, vm, image)
	}
	return nil
}

func (c testCapability) Cleanup(ctx context.Context, vm model.VMRecord) error {
	if c.cleanup != nil {
		return c.cleanup(ctx, vm)
	}
	return nil
}

func (c testCapability) ContributeGuest(builder *guestconfig.Builder, vm model.VMRecord, image model.Image) {
	if c.contribute != nil {
		c.contribute(builder, vm, image)
	}
}

func (c testCapability) ContributeMachine(cfg *firecracker.MachineConfig, vm model.VMRecord, image model.Image) {
	if c.contributeFC != nil {
		c.contributeFC(cfg, vm, image)
	}
}

func (c testCapability) ApplyConfigChange(ctx context.Context, before, after model.VMRecord) error {
	if c.configChange != nil {
		return c.configChange(ctx, before, after)
	}
	return nil
}

func (c testCapability) AddDoctorChecks(ctx context.Context, report *system.Report) {
	if c.doctor != nil {
		c.doctor(ctx, report)
	}
}

func (c testCapability) AddStartPreflight(ctx context.Context, checks *system.Preflight, vm model.VMRecord, image model.Image) {
	if c.startPreflight != nil {
		c.startPreflight(ctx, checks, vm, image)
	}
}

func TestPrepareCapabilityHostsRollsBackPreparedCapabilitiesInReverseOrder(t *testing.T) {
	vm := testVM("devbox", "image", "172.16.0.2")
	var cleanupOrder []string

	d := &Daemon{
		vmCaps: []vmCapability{
			testCapability{
				name: "first",
				prepare: func(context.Context, *model.VMRecord, model.Image) error {
					return nil
				},
				cleanup: func(context.Context, model.VMRecord) error {
					cleanupOrder = append(cleanupOrder, "first")
					return nil
				},
			},
			testCapability{
				name: "second",
				prepare: func(context.Context, *model.VMRecord, model.Image) error {
					return nil
				},
				cleanup: func(context.Context, model.VMRecord) error {
					cleanupOrder = append(cleanupOrder, "second")
					return nil
				},
			},
			testCapability{
				name: "broken",
				prepare: func(context.Context, *model.VMRecord, model.Image) error {
					return errors.New("boom")
				},
			},
		},
	}
	wireServices(d)

	err := d.prepareCapabilityHosts(context.Background(), &vm, model.Image{})
	if err == nil || err.Error() != "boom" {
		t.Fatalf("prepareCapabilityHosts() error = %v, want boom", err)
	}
	if !reflect.DeepEqual(cleanupOrder, []string{"second", "first"}) {
		t.Fatalf("cleanup order = %v, want reverse prepared order", cleanupOrder)
	}
}

func TestContributeHooksPopulateGuestAndMachineConfig(t *testing.T) {
	d := &Daemon{
		vmCaps: []vmCapability{
			testCapability{
				name: "guest",
				contribute: func(builder *guestconfig.Builder, _ model.VMRecord, _ model.Image) {
					builder.AddMount(guestconfig.MountSpec{Source: "/dev/vdb", Target: "/work", FSType: "ext4"})
				},
				contributeFC: func(cfg *firecracker.MachineConfig, _ model.VMRecord, _ model.Image) {
					cfg.Drives = append(cfg.Drives, firecracker.DriveConfig{ID: "work", Path: "/tmp/work.ext4"})
				},
			},
		},
	}
	wireServices(d)

	builder := guestconfig.NewBuilder()
	d.contributeGuestConfig(builder, model.VMRecord{}, model.Image{})

	cfg := firecracker.MachineConfig{Drives: []firecracker.DriveConfig{{ID: "rootfs", Path: "/dev/root", IsRoot: true}}}
	d.contributeMachineConfig(&cfg, model.VMRecord{}, model.Image{})

	fstab := builder.RenderFSTab("")
	if !reflect.DeepEqual(cfg.Drives[1], firecracker.DriveConfig{ID: "work", Path: "/tmp/work.ext4"}) {
		t.Fatalf("machine drives = %+v, want contributed work drive", cfg.Drives)
	}
	if want := "/dev/vdb /work ext4 defaults 0 0\n"; fstab != want {
		t.Fatalf("guest fstab = %q, want %q", fstab, want)
	}
}

func TestProbeBangerDNSAcceptsRealServer(t *testing.T) {
	server, err := vmdns.New("127.0.0.1:0", nil)
	if err != nil {
		t.Fatalf("vmdns.New: %v", err)
	}
	t.Cleanup(func() { _ = server.Close() })

	if !probeBangerDNS(server.Addr()) {
		t.Fatal("probeBangerDNS rejected the real banger DNS server")
	}
}

func TestProbeBangerDNSRejectsSilentListener(t *testing.T) {
	// A UDP listener that drops every datagram. The probe should
	// time out and return false — i.e. "this is not banger".
	conn, err := net.ListenPacket("udp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("ListenPacket: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })

	if probeBangerDNS(conn.LocalAddr().String()) {
		t.Fatal("probeBangerDNS accepted a silent non-DNS listener")
	}
}

func TestDefaultCapabilitiesInOrder(t *testing.T) {
	d := &Daemon{}
	wireServices(d)
	var names []string
	for _, capability := range d.vmCaps {
		names = append(names, capability.Name())
	}
	want := []string{"work-disk", "dns", "nat"}
	if !reflect.DeepEqual(names, want) {
		t.Fatalf("capabilities = %v, want %v", names, want)
	}
}
