package emitter

import (
	"strings"
	"testing"

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

func TestSocketEmitter_RenderSocketLines_NoPorts(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Job",
		Name:    "backup",
		Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/rsync"},
	}
	lines := (SocketEmitter{}).RenderSocketLines(spec)
	if len(lines) != 0 {
		t.Errorf("got %d lines, want 0 for no ports: %v", len(lines), lines)
	}
}

func TestSocketEmitter_RenderSocketLines_NilSpec(t *testing.T) {
	lines := (SocketEmitter{}).RenderSocketLines(nil)
	if lines != nil {
		t.Errorf("nil spec should return nil, got %v", lines)
	}
}

func TestSocketEmitter_RenderSocketLines_SinglePort(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	lines := (SocketEmitter{}).RenderSocketLines(spec)
	// Expect: RuntimeDirectory + comment. No TCP bind (default socket).
	wantRT := "RuntimeDirectory=orca/alloc-web"
	if !contains(lines, wantRT) {
		t.Errorf("lines %v missing %q", lines, wantRT)
	}
	wantSock := "# socket: /run/orca/alloc-web/port-http.sock"
	if !contains(lines, wantSock) {
		t.Errorf("lines %v missing %q", lines, wantSock)
	}
	for _, l := range lines {
		if strings.HasPrefix(l, "ExecStartPre=") {
			t.Errorf("socket bind should not emit ExecStartPre (no TCP opt-in): %s", l)
		}
	}
}

func TestSocketEmitter_RenderSocketLines_MultiplePorts(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind: "Service",
		Name: "api",
		Ports: []jobspec.PortSpec{
			{Name: "http", Port: 8080},
			{Name: "grpc", Port: 9090},
		},
	}
	lines := (SocketEmitter{}).RenderSocketLines(spec)
	// Two RuntimeDirectory lines (one per port).
	count := 0
	for _, l := range lines {
		if l == "RuntimeDirectory=orca/alloc-api" {
			count++
		}
	}
	if count != 2 {
		t.Errorf("RuntimeDirectory count = %d, want 2 (one per port)", count)
	}
	if !contains(lines, "# socket: /run/orca/alloc-api/port-http.sock") {
		t.Errorf("missing http socket comment")
	}
	if !contains(lines, "# socket: /run/orca/alloc-api/port-grpc.sock") {
		t.Errorf("missing grpc socket comment")
	}
}

func TestSocketEmitter_RenderSocketLines_TCPBind127(t *testing.T) {
	// service.bind = 127.0.0.1 → TCP opt-in → ExecStartPre marker per port.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: "127.0.0.1"},
	}
	lines := (SocketEmitter{}).RenderSocketLines(spec)
	found := false
	for _, l := range lines {
		if strings.HasPrefix(l, "ExecStartPre=/bin/echo orca: bind 127.0.0.1 port http (tcp, R-007 opt-in)") {
			found = true
		}
	}
	if !found {
		t.Errorf("missing TCP bind ExecStartPre marker; lines: %v", lines)
	}
}

func TestSocketEmitter_RenderSocketLines_TCPBindIPv6(t *testing.T) {
	// Non-127.0.0.1 bind is accepted by schema but not the documented
	// opt-in; the marker is only emitted for 127.0.0.1. The
	// RuntimeDirectory lines are still emitted (the directory exists
	// regardless of bind mode — sockets or TCP).
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: "::1"},
	}
	lines := (SocketEmitter{}).RenderSocketLines(spec)
	for _, l := range lines {
		if strings.HasPrefix(l, "ExecStartPre=") {
			t.Errorf("::1 bind should NOT emit TCP marker (only 127.0.0.1 is documented opt-in): %s", l)
		}
	}
	if !contains(lines, "RuntimeDirectory=orca/alloc-web") {
		t.Errorf("RuntimeDirectory should still be emitted for ::1 bind")
	}
}

func TestSocketEmitter_RenderSocketLines_EmptyBindSocket(t *testing.T) {
	// Empty bind → default socket → no TCP marker, but RuntimeDirectory emitted.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: ""},
	}
	lines := (SocketEmitter{}).RenderSocketLines(spec)
	for _, l := range lines {
		if strings.HasPrefix(l, "ExecStartPre=") {
			t.Errorf("empty bind should NOT emit TCP marker: %s", l)
		}
	}
	if !contains(lines, "RuntimeDirectory=orca/alloc-web") {
		t.Errorf("RuntimeDirectory missing for empty bind")
	}
}

func TestSocketEmitter_RenderSocketLines_NilService(t *testing.T) {
	// No service block → default socket → no TCP marker, but RuntimeDirectory emitted.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	lines := (SocketEmitter{}).RenderSocketLines(spec)
	for _, l := range lines {
		if strings.HasPrefix(l, "ExecStartPre=") {
			t.Errorf("nil service should NOT emit TCP marker: %s", l)
		}
	}
	if !contains(lines, "RuntimeDirectory=orca/alloc-web") {
		t.Errorf("RuntimeDirectory missing for nil service")
	}
}

func TestSocketEmitter_SocketPath(t *testing.T) {
	got := SocketPath("alloc-123", "http")
	want := "/run/orca/alloc-alloc-123/port-http.sock"
	if got != want {
		t.Errorf("SocketPath = %q, want %q", got, want)
	}
}

func TestSocketEmitter_AllocIDPlaceholderNilSpec(t *testing.T) {
	if got := allocIDForSocket(nil); got != "<allocID>" {
		t.Errorf("allocIDForSocket(nil) = %q, want <allocID>", got)
	}
}

func TestSocketEmitter_AllocIDPlaceholderEmptyName(t *testing.T) {
	spec := &jobspec.WorkloadSpec{Name: "  "}
	if got := allocIDForSocket(spec); got != "<allocID>" {
		t.Errorf("allocIDForSocket(empty name) = %q, want <allocID>", got)
	}
}

func TestSocketEmitter_AllocIDPlaceholderNamedSpec(t *testing.T) {
	spec := &jobspec.WorkloadSpec{Name: "web"}
	if got := allocIDForSocket(spec); got != "web" {
		t.Errorf("allocIDForSocket(web) = %q, want web", got)
	}
}

func TestSocketEmitter_SocketPathPlaceholder(t *testing.T) {
	got := SocketPath("<allocID>", "grpc")
	want := "/run/orca/alloc-<allocID>/port-grpc.sock"
	if got != want {
		t.Errorf("SocketPath = %q, want %q", got, want)
	}
}

func TestSystemdEmitter_IntegratesSocketLines(t *testing.T) {
	// End-to-end: the systemd unit for a Service with ports contains
	// the RuntimeDirectory line emitted by the SocketEmitter.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/httpd"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	files, err := SystemdEmitter{}.Render(spec, &Node{})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	c := files[0].Content
	if !strings.Contains(c, "RuntimeDirectory=orca/alloc-web\n") {
		t.Errorf("unit missing RuntimeDirectory line\n%s", c)
	}
	if !strings.Contains(c, "# socket: /run/orca/alloc-web/port-http.sock\n") {
		t.Errorf("unit missing socket path comment\n%s", c)
	}
}

func TestSystemdEmitter_IntegratesSocketLinesTCPBind(t *testing.T) {
	// When service.bind = 127.0.0.1, the unit contains the ExecStartPre
	// TCP-bind marker.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/httpd"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: "127.0.0.1"},
	}
	files, err := SystemdEmitter{}.Render(spec, &Node{})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	c := files[0].Content
	if !strings.Contains(c, "ExecStartPre=/bin/echo orca: bind 127.0.0.1 port http (tcp, R-007 opt-in)\n") {
		t.Errorf("unit missing TCP bind ExecStartPre marker\n%s", c)
	}
}

func TestSystemdEmitter_NoSocketLinesForPortlessSpec(t *testing.T) {
	// A Job with no ports → no RuntimeDirectory line in the unit.
	spec := &jobspec.WorkloadSpec{
		Kind:    "Job",
		Name:    "backup",
		Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/rsync"},
	}
	files, err := SystemdEmitter{}.Render(spec, &Node{})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	c := files[0].Content
	if strings.Contains(c, "RuntimeDirectory=") {
		t.Errorf("portless spec should not emit RuntimeDirectory\n%s", c)
	}
	if strings.Contains(c, "# socket:") {
		t.Errorf("portless spec should not emit socket comment\n%s", c)
	}
}

// contains reports whether the slice contains the string s.
func contains(lines []string, s string) bool {
	for _, l := range lines {
		if l == s {
			return true
		}
	}
	return false
}
