package runtime

import (
	"context"
	"testing"
	"time"
)

// TestPveVMRuntime_HappyPath verifies the qm lifecycle.
func TestPveVMRuntime_HappyPath(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("qm create", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("qm start", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("qm shutdown", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("qm stop", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("qm status", func(cmd string) ([]byte, int) {
		return []byte("status: running\n"), 0
	})

	tr := realTransport(t, srv)
	defer tr.Close()
	v := NewPveVMRuntime(tr)
	a := allocWithNode("pve-vm", "local:vmdir/disk.qcow2", "", srv.addr())

	ctx, cancel := withTimeout(10 * time.Second)
	defer cancel()
	if err := v.Prepare(ctx, a); err != nil {
		t.Fatalf("Prepare: %v", err)
	}
	pid, err := v.Start(ctx, a)
	if err != nil {
		t.Fatalf("Start: %v", err)
	}
	if pid <= 0 {
		t.Fatalf("pid = %d, want > 0", pid)
	}
	st, err := v.Status(ctx, a)
	if err != nil {
		t.Fatalf("Status: %v", err)
	}
	if st != StateRunning {
		t.Errorf("Status = %q, want running", st)
	}
	if err := v.Stop(ctx, a); err != nil {
		t.Fatalf("Stop: %v", err)
	}
}

// TestPveVMRuntime_StatusStopped verifies Status maps "stopped".
func TestPveVMRuntime_StatusStopped(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("qm status", func(cmd string) ([]byte, int) {
		return []byte("status: stopped\n"), 0
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	v := NewPveVMRuntime(tr)
	a := allocWithNode("pve-vm", "img", "", srv.addr())
	st, _ := v.Status(context.Background(), a)
	if st != StateStopped {
		t.Errorf("Status = %q, want stopped", st)
	}
}

// TestPveVMRuntime_StatusUnknown verifies Status returns pending on
// unrecognized output.
func TestPveVMRuntime_StatusUnknown(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("qm status", func(cmd string) ([]byte, int) {
		return []byte("weird state\n"), 0
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	v := NewPveVMRuntime(tr)
	a := allocWithNode("pve-vm", "img", "", srv.addr())
	st, _ := v.Status(context.Background(), a)
	if st != StatePending {
		t.Errorf("Status = %q, want pending", st)
	}
}

// TestPveVMRuntime_PrepareNoImage verifies Prepare errors with no disk.
func TestPveVMRuntime_PrepareNoImage(t *testing.T) {
	v := NewPveVMRuntime(nil)
	a := allocNoImage("pve-vm")
	if err := v.Prepare(context.Background(), a); err == nil {
		t.Error("Prepare with no disk should error")
	}
}

// TestPveVMRuntime_PrepareNilRuntime verifies Prepare errors when
// the Spec has no Runtime block at all.
func TestPveVMRuntime_PrepareNilRuntime(t *testing.T) {
	v := NewPveVMRuntime(nil)
	a := &Alloc{ID: "x", Runtime: "pve-vm", Spec: nil}
	if err := v.Prepare(context.Background(), a); err == nil {
		t.Error("Prepare with nil spec should error")
	}
}

// TestPveVMRuntime_StartError verifies Start propagates qm start errors.
func TestPveVMRuntime_StartError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("qm start", func(cmd string) ([]byte, int) {
		return []byte("already running\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	v := NewPveVMRuntime(tr)
	a := allocWithNode("pve-vm", "img", "", srv.addr())
	if _, err := v.Start(context.Background(), a); err == nil {
		t.Error("Start with qm error should error")
	}
}

// TestPveVMRuntime_StopError verifies Stop errors on qm stop failure.
func TestPveVMRuntime_StopError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("qm shutdown", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("qm stop", func(cmd string) ([]byte, int) {
		return []byte("vm locked\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	v := NewPveVMRuntime(tr)
	a := allocWithNode("pve-vm", "img", "", srv.addr())
	if err := v.Stop(context.Background(), a); err == nil {
		t.Error("Stop with qm error should error")
	}
}

// TestPveVMRuntime_StatusError verifies Status returns failed on qm
// status error.
func TestPveVMRuntime_StatusError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("qm status", func(cmd string) ([]byte, int) {
		return []byte("no such vm\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	v := NewPveVMRuntime(tr)
	a := allocWithNode("pve-vm", "img", "", srv.addr())
	st, err := v.Status(context.Background(), a)
	if err == nil {
		t.Error("Status with qm error should error")
	}
	if st != StateFailed {
		t.Errorf("Status = %q, want failed", st)
	}
}

// TestPveVMRuntime_PrepareError verifies Prepare propagates qm create
// errors.
func TestPveVMRuntime_PrepareError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("qm create", func(cmd string) ([]byte, int) {
		return []byte("vmid already exists\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	v := NewPveVMRuntime(tr)
	a := allocWithNode("pve-vm", "img", "", srv.addr())
	if err := v.Prepare(context.Background(), a); err == nil {
		t.Error("Prepare with qm create error should error")
	}
}

// --- PveCTRuntime ---

func TestPveCTRuntime_HappyPath(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("pct create", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("pct start", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("pct shutdown", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("pct stop", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("pct status", func(cmd string) ([]byte, int) {
		return []byte("status: running\n"), 0
	})

	tr := realTransport(t, srv)
	defer tr.Close()
	c := NewPveCTRuntime(tr)
	a := allocWithNode("pve-ct", "local:vztmpl/alpine.tar.xz", "", srv.addr())

	ctx, cancel := withTimeout(10 * time.Second)
	defer cancel()
	if err := c.Prepare(ctx, a); err != nil {
		t.Fatalf("Prepare: %v", err)
	}
	pid, err := c.Start(ctx, a)
	if err != nil {
		t.Fatalf("Start: %v", err)
	}
	if pid <= 0 {
		t.Fatalf("pid = %d, want > 0", pid)
	}
	st, err := c.Status(ctx, a)
	if err != nil {
		t.Fatalf("Status: %v", err)
	}
	if st != StateRunning {
		t.Errorf("Status = %q, want running", st)
	}
	if err := c.Stop(ctx, a); err != nil {
		t.Fatalf("Stop: %v", err)
	}
}

// TestPveCTRuntime_StatusStopped verifies pct status stopped.
func TestPveCTRuntime_StatusStopped(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("pct status", func(cmd string) ([]byte, int) {
		return []byte("status: stopped\n"), 0
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	c := NewPveCTRuntime(tr)
	a := allocWithNode("pve-ct", "tmpl", "", srv.addr())
	st, _ := c.Status(context.Background(), a)
	if st != StateStopped {
		t.Errorf("Status = %q, want stopped", st)
	}
}

// TestPveCTRuntime_StatusUnknown verifies pending on unrecognized
// output.
func TestPveCTRuntime_StatusUnknown(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("pct status", func(cmd string) ([]byte, int) {
		return []byte("unknown\n"), 0
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	c := NewPveCTRuntime(tr)
	a := allocWithNode("pve-ct", "tmpl", "", srv.addr())
	st, _ := c.Status(context.Background(), a)
	if st != StatePending {
		t.Errorf("Status = %q, want pending", st)
	}
}

// TestPveCTRuntime_PrepareNoImage verifies Prepare errors with no
// template.
func TestPveCTRuntime_PrepareNoImage(t *testing.T) {
	c := NewPveCTRuntime(nil)
	a := allocNoImage("pve-ct")
	if err := c.Prepare(context.Background(), a); err == nil {
		t.Error("Prepare with no template should error")
	}
}

// TestPveCTRuntime_PrepareNilRuntime verifies Prepare errors on nil
// spec.
func TestPveCTRuntime_PrepareNilRuntime(t *testing.T) {
	c := NewPveCTRuntime(nil)
	a := &Alloc{ID: "x", Runtime: "pve-ct", Spec: nil}
	if err := c.Prepare(context.Background(), a); err == nil {
		t.Error("Prepare with nil spec should error")
	}
}

// TestPveCTRuntime_StartError verifies Start errors on pct start fail.
func TestPveCTRuntime_StartError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("pct start", func(cmd string) ([]byte, int) {
		return []byte("already running\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	c := NewPveCTRuntime(tr)
	a := allocWithNode("pve-ct", "tmpl", "", srv.addr())
	if _, err := c.Start(context.Background(), a); err == nil {
		t.Error("Start with pct error should error")
	}
}

// TestPveCTRuntime_StopError verifies Stop errors on pct stop fail.
func TestPveCTRuntime_StopError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("pct shutdown", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("pct stop", func(cmd string) ([]byte, int) {
		return []byte("container locked\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	c := NewPveCTRuntime(tr)
	a := allocWithNode("pve-ct", "tmpl", "", srv.addr())
	if err := c.Stop(context.Background(), a); err == nil {
		t.Error("Stop with pct error should error")
	}
}

// TestPveCTRuntime_StatusError verifies Status failed on pct status
// error.
func TestPveCTRuntime_StatusError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("pct status", func(cmd string) ([]byte, int) {
		return []byte("no such container\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	c := NewPveCTRuntime(tr)
	a := allocWithNode("pve-ct", "tmpl", "", srv.addr())
	st, err := c.Status(context.Background(), a)
	if err == nil {
		t.Error("Status with pct error should error")
	}
	if st != StateFailed {
		t.Errorf("Status = %q, want failed", st)
	}
}

// TestVMIDFor verifies the VMID is deterministic and within range.
func TestVMIDFor(t *testing.T) {
	a := &Alloc{ID: "alloc-1"}
	id := vmidFor(a)
	if id < 100 || id > 99999 {
		t.Errorf("vmidFor = %d, want in [100, 99999]", id)
	}
	// stability
	if vmidFor(a) != id {
		t.Error("vmidFor not stable")
	}
	// two allocs should differ
	b := &Alloc{ID: "alloc-2"}
	if vmidFor(b) == id {
		t.Logf("note: two allocs collided on vmid (rare but allowed)")
	}
}
