package scheduler

import "testing"

func TestEvaluateConstraint_Equality(t *testing.T) {
	node := NodeInfo{Hostname: "h-1", Kind: "linux", CPU: 4, Memory: 4096, Tags: []string{"web"}, Runtimes: []string{"process"}}
	cases := []struct {
		name string
		expr string
		want bool
	}{
		{"hostname eq", `node.hostname == "h-1"`, true},
		{"hostname ne", `node.hostname == "h-2"`, false},
		{"kind eq", `node.kind == "linux"`, true},
		{"kind ne", `node.kind == "proxmox"`, false},
		{"cpus eq", `node.cpus == 4`, true},
		{"memory eq", `node.memory == 4096`, true},
	}
	for _, c := range cases {
		got, err := EvaluateConstraint(c.expr, node)
		if err != nil {
			t.Errorf("%s: %v", c.name, err)
			continue
		}
		if got != c.want {
			t.Errorf("%s: got %v, want %v", c.name, got, c.want)
		}
	}
}

func TestEvaluateConstraint_Comparison(t *testing.T) {
	node := NodeInfo{Hostname: "h", Kind: "linux", CPU: 4, Memory: 4096}
	cases := []struct {
		expr string
		want bool
	}{
		{"node.cpus >= 2", true},
		{"node.cpus >= 4", true},
		{"node.cpus > 4", false},
		{"node.cpus > 2", true},
		{"node.cpus <= 4", true},
		{"node.cpus < 2", false},
		{"node.cpus != 8", true},
		{"node.cpus == 8", false},
		{"node.memory >= 2048", true},
		{"node.memory < 1024", false},
	}
	for _, c := range cases {
		got, err := EvaluateConstraint(c.expr, node)
		if err != nil {
			t.Errorf("%q: %v", c.expr, err)
			continue
		}
		if got != c.want {
			t.Errorf("%q: got %v, want %v", c.expr, got, c.want)
		}
	}
}

func TestEvaluateConstraint_Membership(t *testing.T) {
	node := NodeInfo{Tags: []string{"web", "log-shipper"}, Runtimes: []string{"process", "wasmtime"}}
	cases := []struct {
		expr string
		want bool
	}{
		{`"web" in node.tags`, true},
		{`"missing" in node.tags`, false},
		{`"process" in node.runtimes`, true},
		{`"podman" in node.runtimes`, false},
		{`"log-shipper" not in node.tags`, false},
		{`"missing" not in node.tags`, true},
	}
	for _, c := range cases {
		got, err := EvaluateConstraint(c.expr, node)
		if err != nil {
			t.Errorf("%q: %v", c.expr, err)
			continue
		}
		if got != c.want {
			t.Errorf("%q: got %v, want %v", c.expr, got, c.want)
		}
	}
}

func TestEvaluateConstraint_BooleanComposition(t *testing.T) {
	node := NodeInfo{Kind: "linux", CPU: 4, Tags: []string{"web"}}
	cases := []struct {
		expr string
		want bool
	}{
		{`node.kind == "linux" and node.cpus >= 2`, true},
		{`node.kind == "proxmox" and node.cpus >= 2`, false},
		{`node.kind == "linux" or node.kind == "proxmox"`, true},
		{`node.kind == "proxmox" or node.kind == "linux"`, true},
		{`not node.kind == "proxmox"`, true},
		{`not node.kind == "linux"`, false},
		{`(node.kind == "linux") and (node.cpus >= 2)`, true},
		{`node.cpus >= 2 and not "blocked" in node.tags`, true},
		{`node.kind == "linux" and node.cpus >= 2 and "web" in node.tags`, true},
		{`node.kind == "linux" or node.kind == "proxmox" or node.cpus > 100`, true},
	}
	for _, c := range cases {
		got, err := EvaluateConstraint(c.expr, node)
		if err != nil {
			t.Errorf("%q: %v", c.expr, err)
			continue
		}
		if got != c.want {
			t.Errorf("%q: got %v, want %v", c.expr, got, c.want)
		}
	}
}

func TestEvaluateConstraint_BareIdentifiers(t *testing.T) {
	// Bare identifiers resolve through the same attribute map as
	// node.<field> (per PRD: constraints may use either form).
	node := NodeInfo{Kind: "linux", CPU: 4}
	got, err := EvaluateConstraint(`kind == "linux"`, node)
	if err != nil {
		t.Fatalf("bare kind: %v", err)
	}
	if !got {
		t.Error("bare kind == linux: got false, want true")
	}
}

func TestEvaluateConstraint_TrueFalseLiterals(t *testing.T) {
	node := NodeInfo{}
	cases := []struct {
		expr string
		want bool
	}{
		{"true", true},
		{"false", false},
		{"not false", true},
		{"not true", false},
		{"true and true", true},
		{"true and false", false},
		{"false or true", true},
	}
	for _, c := range cases {
		got, err := EvaluateConstraint(c.expr, node)
		if err != nil {
			t.Errorf("%q: %v", c.expr, err)
			continue
		}
		if got != c.want {
			t.Errorf("%q: got %v, want %v", c.expr, got, c.want)
		}
	}
}

func TestEvaluateConstraint_Errors(t *testing.T) {
	node := NodeInfo{Kind: "linux"}
	cases := []struct {
		name string
		expr string
	}{
		{"empty", ""},
		{"unterminated string", `node.kind == "linux`},
		{"unknown attribute", `node.bogus == 1`},
		{"unknown bare attr", `bogus == 1`},
		{"dotted on non-node", `host.kind == "linux"`},
		{"bad operator", `node.cpus + 2`},
		{"trailing input", `node.kind == "linux" garbage`},
		{"unbalanced paren", `(node.kind == "linux"`},
		{"missing rhs", `node.cpus >=`},
		{"not without in", `"x" not node.tags`},
		{"ordered compare on bool", `true < false`},
		{"ordered compare on mismatched types", `node.kind > 2`},
		{"in on non-list", `"x" in node.kind`},
	}
	for _, c := range cases {
		_, err := EvaluateConstraint(c.expr, node)
		if err == nil {
			t.Errorf("%s: expected error for %q, got nil", c.name, c.expr)
		}
	}
}

func TestEvaluateAll(t *testing.T) {
	node := NodeInfo{Kind: "linux", CPU: 4, Tags: []string{"web"}}
	cases := []struct {
		name        string
		constraints []string
		want        bool
	}{
		{"empty", nil, true},
		{"all pass", []string{`node.kind == "linux"`, "node.cpus >= 2"}, true},
		{"one fails", []string{`node.kind == "linux"`, "node.cpus >= 8"}, false},
		{"all fail", []string{`node.kind == "proxmox"`, "node.cpus >= 8"}, false},
	}
	for _, c := range cases {
		got, err := EvaluateAll(c.constraints, node)
		if err != nil {
			t.Errorf("%s: %v", c.name, err)
			continue
		}
		if got != c.want {
			t.Errorf("%s: got %v, want %v", c.name, got, c.want)
		}
	}
}

func TestEvaluateAll_PropagatesError(t *testing.T) {
	node := NodeInfo{}
	if _, err := EvaluateAll([]string{"bogus == 1"}, node); err == nil {
		t.Error("EvaluateAll: expected error for malformed constraint")
	}
}
