package engine

import (
	"context"
	"testing"

	"git.cloudinit.dev/coreci/orca/internal/store"
)

func TestPickNodeBestFit(t *testing.T) {
	caps := []*store.NodeCapacity{
		{NodeID: "node-b", CPUMillicores: 1000, MemoryMiB: 1024, DiskMiB: 1024},
		{NodeID: "node-a", CPUMillicores: 4000, MemoryMiB: 4096, DiskMiB: 4096},
		{NodeID: "node-c", CPUMillicores: 500, MemoryMiB: 512, DiskMiB: 512},
	}
	spec := JobSpec{CPUMillicores: 1000, MemoryMiB: 1024, DiskMiB: 1024}
	got, idx, err := PickNode(spec, caps)
	if err != nil {
		t.Fatalf("PickNode: %v", err)
	}
	if got.NodeID != "node-a" {
		t.Errorf("PickNode: got %s, want node-a (most free capacity)", got.NodeID)
	}
	if idx != 1 {
		t.Errorf("PickNode: got idx %d, want 1", idx)
	}
}

func TestPickNodeNoFit(t *testing.T) {
	caps := []*store.NodeCapacity{
		{NodeID: "node-a", CPUMillicores: 100, MemoryMiB: 100, DiskMiB: 100},
	}
	spec := JobSpec{CPUMillicores: 1000, MemoryMiB: 1024, DiskMiB: 1024}
	_, _, err := PickNode(spec, caps)
	if err == nil {
		t.Fatal("expected PickNode to fail when no node can fit")
	}
}

func TestPickNodeTieDeterministic(t *testing.T) {
	// Two nodes with identical free capacity. Tie broken by NodeID
	// (lexicographic) for determinism.
	caps := []*store.NodeCapacity{
		{NodeID: "node-z", CPUMillicores: 4000, MemoryMiB: 4096, DiskMiB: 4096},
		{NodeID: "node-a", CPUMillicores: 4000, MemoryMiB: 4096, DiskMiB: 4096},
	}
	spec := JobSpec{CPUMillicores: 1000, MemoryMiB: 1024, DiskMiB: 1024}
	got, _, err := PickNode(spec, caps)
	if err != nil {
		t.Fatalf("PickNode: %v", err)
	}
	if got.NodeID != "node-a" {
		t.Errorf("PickNode tie-break: got %s, want node-a (lexicographic)", got.NodeID)
	}
}

func TestJobSpecFits(t *testing.T) {
	spec := JobSpec{CPUMillicores: 1000, MemoryMiB: 1024, DiskMiB: 1024}
	c := &store.NodeCapacity{CPUMillicores: 2000, MemoryMiB: 2048, DiskMiB: 2048}
	if !spec.Fits(c) {
		t.Error("Fits: should fit")
	}
	c.CPUMillicores = 500
	if spec.Fits(c) {
		t.Error("Fits: should not fit (CPU too low)")
	}
}

func TestJobSpecFits_NilCapacity(t *testing.T) {
	spec := JobSpec{CPUMillicores: 1000}
	if spec.Fits(nil) {
		t.Error("Fits(nil): should be false")
	}
}

func TestJobSpecScore_NilCapacity(t *testing.T) {
	spec := JobSpec{CPUMillicores: 1000, MemoryMiB: 1024}
	if got := spec.Score(nil); got != -1 {
		t.Errorf("Score(nil) = %d, want -1", got)
	}
}

func TestJobSpecScore_OverCapacity(t *testing.T) {
	spec := JobSpec{CPUMillicores: 2000, MemoryMiB: 1024}
	c := &store.NodeCapacity{CPUMillicores: 1000, MemoryMiB: 2048}
	if got := spec.Score(c); got != -1 {
		t.Errorf("Score over CPU = %d, want -1", got)
	}
	c2 := &store.NodeCapacity{CPUMillicores: 4000, MemoryMiB: 512}
	if got := spec.Score(c2); got != -1 {
		t.Errorf("Score over Mem = %d, want -1", got)
	}
}

func TestJobSpecScore_Fits(t *testing.T) {
	spec := JobSpec{CPUMillicores: 1000, MemoryMiB: 1024}
	c := &store.NodeCapacity{CPUMillicores: 4000, MemoryMiB: 4096}
	got := spec.Score(c)
	want := int64((4000 - 1000) + (4096 - 1024))
	if got != want {
		t.Errorf("Score = %d, want %d", got, want)
	}
}

func TestPickNode_Empty(t *testing.T) {
	_, _, err := PickNode(JobSpec{}, nil)
	if err == nil {
		t.Fatal("expected error for empty capacities")
	}
}

func TestMemLocalNode_Capacity(t *testing.T) {
	c := &store.NodeCapacity{NodeID: "self", CPUMillicores: 1000, MemoryMiB: 1024}
	ln := MemLocalNode(c)
	got, err := ln.Capacity(context.Background())
	if err != nil {
		t.Fatalf("Capacity: %v", err)
	}
	if got != c {
		t.Errorf("Capacity: got %+v, want %+v", got, c)
	}
}

func TestMemLocalNode_NilCapacity(t *testing.T) {
	ln := MemLocalNode(nil)
	_, err := ln.Capacity(context.Background())
	if err == nil {
		t.Fatal("expected error for nil capacity")
	}
}
