package emitter

import (
	"strings"
	"testing"

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

func TestTraefikEmitter_RenderBasic(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Health:  &jobspec.HealthBlock{CheckType: "http", Interval: "5s", Timeout: "1s"},
	}
	node := &Node{Hostname: "node-1", Runtime: []string{"process"}}
	files, err := TraefikEmitter{}.Render(spec, node)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if len(files) != 1 {
		t.Fatalf("got %d files, want 1", len(files))
	}
	f := files[0]
	wantPath := "/etc/traefik/dynamic/orca-web.yaml"
	if f.Path != wantPath {
		t.Errorf("Path = %q, want %q", f.Path, wantPath)
	}
	if f.Mode != "0644" {
		t.Errorf("Mode = %q, want 0644", f.Mode)
	}
	c := f.Content
	if !strings.Contains(c, "http:") {
		t.Errorf("content missing 'http:'\n%s", c)
	}
	if !strings.Contains(c, "routers:") {
		t.Errorf("content missing 'routers:'\n%s", c)
	}
	if !strings.Contains(c, "orca-web:") {
		t.Errorf("content missing 'orca-web:' router/service key\n%s", c)
	}
	if !strings.Contains(c, `rule: PathPrefix("/web")`) {
		t.Errorf("content missing PathPrefix rule\n%s", c)
	}
	if !strings.Contains(c, "services:") {
		t.Errorf("content missing 'services:'\n%s", c)
	}
	if !strings.Contains(c, "loadBalancer:") {
		t.Errorf("content missing 'loadBalancer:'\n%s", c)
	}
	if !strings.Contains(c, "unix:///run/orca/alloc-node-1/port-http.sock") {
		t.Errorf("content missing socket server URL\n%s", c)
	}
	if !strings.Contains(c, "certResolver: orca") {
		t.Errorf("content missing 'certResolver: orca'\n%s", c)
	}
	if !strings.Contains(c, "domains:") {
		t.Errorf("content missing TLS domains\n%s", c)
	}
	if !strings.Contains(c, "healthCheck:") {
		t.Errorf("content missing 'healthCheck:'\n%s", c)
	}
	if !strings.Contains(c, "interval: 5s") {
		t.Errorf("content missing 'interval: 5s'\n%s", c)
	}
	if !strings.Contains(c, "timeout: 1s") {
		t.Errorf("content missing 'timeout: 1s'\n%s", c)
	}
}

func TestTraefikEmitter_RenderMultiplePorts(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "api",
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Ports: []jobspec.PortSpec{
			{Name: "http", Port: 8080},
			{Name: "grpc", Port: 9090},
		},
	}
	node := &Node{Hostname: "n1"}
	files, err := TraefikEmitter{}.Render(spec, node)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	c := files[0].Content
	if !strings.Contains(c, "port-http.sock") {
		t.Errorf("missing http socket: %s", c)
	}
	if !strings.Contains(c, "port-grpc.sock") {
		t.Errorf("missing grpc socket: %s", c)
	}
}

func TestTraefikEmitter_RenderDrain(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	node := &Node{Hostname: "n1"}
	files, err := TraefikEmitter{}.RenderDrain(spec, node)
	if err != nil {
		t.Fatalf("RenderDrain: %v", err)
	}
	if len(files) != 1 {
		t.Fatalf("got %d files, want 1", len(files))
	}
	c := files[0].Content
	if !strings.Contains(c, "weight: 0") {
		t.Errorf("drain config missing 'weight: 0'\n%s", c)
	}
	if !strings.Contains(c, "unix:///run/orca/alloc-n1/port-http.sock") {
		t.Errorf("drain config missing socket URL\n%s", c)
	}
}

func TestTraefikEmitter_RenderLiveHasNoWeightZero(t *testing.T) {
	// Sanity: the live (non-drain) render must NOT emit `weight: 0`.
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	node := &Node{Hostname: "n1"}
	files, err := TraefikEmitter{}.Render(spec, node)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if strings.Contains(files[0].Content, "weight: 0") {
		t.Errorf("live config should not contain 'weight: 0'\n%s", files[0].Content)
	}
}

func TestTraefikEmitter_RenderNoHealthOmitsHealthCheck(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	node := &Node{Hostname: "n1"}
	files, err := TraefikEmitter{}.Render(spec, node)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if strings.Contains(files[0].Content, "healthCheck:") {
		t.Errorf("config without Health should omit 'healthCheck:'\n%s", files[0].Content)
	}
}

func TestTraefikEmitter_NilSpec(t *testing.T) {
	_, err := TraefikEmitter{}.Render(nil, &Node{})
	if err == nil {
		t.Fatal("expected error for nil spec")
	}
}

func TestTraefikEmitter_EmptyName(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "  ",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	_, err := TraefikEmitter{}.Render(spec, &Node{})
	if err == nil {
		t.Fatal("expected error for empty name")
	}
}

func TestTraefikEmitter_NoPorts(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind: "Service",
		Name: "web",
	}
	_, err := TraefikEmitter{}.Render(spec, &Node{})
	if err == nil {
		t.Fatal("expected error for missing ports")
	}
	if !strings.Contains(err.Error(), "no ports") {
		t.Errorf("error = %q, want 'no ports'", err.Error())
	}
}

func TestTraefikEmitter_NoPortsDrain(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind: "Service",
		Name: "web",
	}
	_, err := TraefikEmitter{}.RenderDrain(spec, &Node{})
	if err == nil {
		t.Fatal("expected error for missing ports on drain")
	}
}

func TestTraefikEmitter_InvalidBind(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: "not-an-ip"},
	}
	_, err := TraefikEmitter{}.Render(spec, &Node{})
	if err == nil {
		t.Fatal("expected error for invalid service.bind")
	}
	if !strings.Contains(err.Error(), "valid IP") {
		t.Errorf("error = %q, want 'valid IP'", err.Error())
	}
}

func TestTraefikEmitter_ValidBindLoopback(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Service: &jobspec.ServiceBlock{Bind: "127.0.0.1"},
	}
	_, err := TraefikEmitter{}.Render(spec, &Node{})
	if err != nil {
		t.Fatalf("127.0.0.1 should be accepted, got %v", err)
	}
}

func TestTraefikEmitter_NilNodeAllocPlaceholder(t *testing.T) {
	// With a nil node, the alloc-id placeholder is the literal
	// "<allocID>" sentinel so the rendered config is still valid YAML
	// (the P08 socket layer substitutes the real alloc-id).
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	files, err := TraefikEmitter{}.Render(spec, nil)
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if !strings.Contains(files[0].Content, "alloc-<allocID>") {
		t.Errorf("nil node should render alloc-<allocID> placeholder\n%s", files[0].Content)
	}
}

func TestTraefikEmitter_EmptyHostnameAllocPlaceholder(t *testing.T) {
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	files, err := TraefikEmitter{}.Render(spec, &Node{Hostname: "  "})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if !strings.Contains(files[0].Content, "alloc-<allocID>") {
		t.Errorf("empty hostname should render alloc-<allocID> placeholder\n%s", files[0].Content)
	}
}

func TestTraefikEmitter_PathNotOrcaV1Prefixed(t *testing.T) {
	// REQ-090: the orca-v1- prefix is only for systemd units; Traefik
	// dynamic-config paths are named orca-<spec.Name>.yaml (single
	// source of truth — no dual-write window for Traefik configs).
	spec := &jobspec.WorkloadSpec{
		Kind:  "Service",
		Name:  "web",
		Ports: []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	files, err := TraefikEmitter{}.Render(spec, &Node{Hostname: "n1"})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if strings.Contains(files[0].Path, "orca-v1-") {
		t.Errorf("Path %q should NOT contain the orca-v1- prefix (systemd-only)", files[0].Path)
	}
	if !strings.HasPrefix(files[0].Path, "/etc/traefik/dynamic/orca-") {
		t.Errorf("Path %q should start with /etc/traefik/dynamic/orca-", files[0].Path)
	}
	if !strings.HasSuffix(files[0].Path, ".yaml") {
		t.Errorf("Path %q should end with .yaml", files[0].Path)
	}
}

func TestTraefikEmitter_RenderYAMLHasRoutersServicesTLS(t *testing.T) {
	// Aggregate structural assertion: the rendered YAML has the four
	// top-level Traefik concepts (routers, services, tls, servers).
	spec := &jobspec.WorkloadSpec{
		Kind:   "Service",
		Name:   "web",
		Ports:  []jobspec.PortSpec{{Name: "http", Port: 8080}},
		Health: &jobspec.HealthBlock{CheckType: "http"},
	}
	files, err := TraefikEmitter{}.Render(spec, &Node{Hostname: "n1"})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	c := files[0].Content
	for _, want := range []string{"routers:", "services:", "tls:", "servers:", "url:"} {
		if !strings.Contains(c, want) {
			t.Errorf("rendered YAML missing %q\n%s", want, c)
		}
	}
}

func TestRegisterTraefik_AllServiceRuntimes(t *testing.T) {
	r := NewRegistry()
	RegisterTraefik(r)
	spec := func(runtime string) *jobspec.WorkloadSpec {
		return &jobspec.WorkloadSpec{
			Kind:    "Service",
			Name:    "web",
			Runtime: &jobspec.RuntimeBlock{OneOf: runtime},
			Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
		}
	}
	for _, runtime := range []string{"process", "podman", "wasm"} {
		t.Run(runtime, func(t *testing.T) {
			files, err := r.Render(spec(runtime), &Node{Hostname: "n1"})
			if err != nil {
				t.Fatalf("Render(service:%s): %v", runtime, err)
			}
			if len(files) != 1 {
				t.Fatalf("got %d files, want 1", len(files))
			}
			if !strings.Contains(files[0].Path, "/etc/traefik/dynamic/orca-web.yaml") {
				t.Errorf("Path = %q", files[0].Path)
			}
		})
	}
}

func TestRegisterTraefik_OverwritesExisting(t *testing.T) {
	// RegisterTraefik should overwrite any prior registration (the
	// Registry documents last-wins).
	r := NewRegistry()
	r.Register("service:process", mockEmitter{files: []File{{Path: "/old"}}})
	RegisterTraefik(r)
	spec := &jobspec.WorkloadSpec{
		Kind:    "Service",
		Name:    "web",
		Runtime: &jobspec.RuntimeBlock{OneOf: "process"},
		Ports:   []jobspec.PortSpec{{Name: "http", Port: 8080}},
	}
	files, err := r.Render(spec, &Node{Hostname: "n1"})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	if files[0].Path == "/old" {
		t.Errorf("RegisterTraefik did not overwrite the prior registration")
	}
}

// Compile-time assertion that TraefikEmitter implements Emitter.
var _ Emitter = TraefikEmitter{}

func TestTraefikEmitter_RenderStaticConfigHybrid(t *testing.T) {
	files, err := TraefikEmitter{}.RenderTraefikStaticConfig(TraefikStaticOpts{})
	if err != nil {
		t.Fatalf("RenderTraefikStaticConfig: %v", err)
	}
	if len(files) != 1 {
		t.Fatalf("got %d files, want 1", len(files))
	}
	f := files[0]
	if f.Path != "/etc/traefik/traefik.yml" {
		t.Errorf("Path = %q, want /etc/traefik/traefik.yml", f.Path)
	}
	if f.Mode != "0644" {
		t.Errorf("Mode = %q, want 0644", f.Mode)
	}
	c := f.Content
	for _, want := range []string{
		"entryPoints:",
		`address: "127.0.0.1:8443"`,
		`address: "127.0.0.1:8080"`,
		`address: "127.0.0.1:8081"`,
		"providers:",
		"file:",
		`filename: "/etc/traefik/dynamic/orca.yml"`,
		"watch: true",
		"log:",
		"level: INFO",
		"format: json",
		"accessLog:",
	} {
		if !strings.Contains(c, want) {
			t.Errorf("static config missing %q:\n%s", want, c)
		}
	}
}

func TestTraefikEmitter_RenderStaticConfigPublicBinding(t *testing.T) {
	files, err := TraefikEmitter{}.RenderTraefikStaticConfig(TraefikStaticOpts{PublicBinding: "traefik-on-public-ip"})
	if err != nil {
		t.Fatalf("Render: %v", err)
	}
	c := files[0].Content
	if !strings.Contains(c, `address: ":443"`) {
		t.Errorf("public binding :443 missing:\n%s", c)
	}
	if !strings.Contains(c, `address: ":80"`) {
		t.Errorf("public binding :80 missing:\n%s", c)
	}
	if strings.Contains(c, `address: "127.0.0.1:8443"`) {
		t.Errorf("hybrid bind should not be present under public binding:\n%s", c)
	}
}
