package cli

import (
	"bytes"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func writeMDSpec(t *testing.T, content string) string {
	t.Helper()
	dir := t.TempDir()
	p := filepath.Join(dir, "spec.md")
	if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
		t.Fatalf("write spec: %v", err)
	}
	return p
}

func writeHCLSpec(t *testing.T, content string) string {
	t.Helper()
	dir := t.TempDir()
	p := filepath.Join(dir, "spec.hcl")
	if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
		t.Fatalf("write spec: %v", err)
	}
	return p
}

const validJobMD = "---\n" +
	"kind: Job\n" +
	"name: my-job\n" +
	"runtime:\n" +
	"  one_of: process\n" +
	"  command: /bin/true\n" +
	"---\n" +
	"# My Job\n\nRuns /bin/true.\n"

const validServiceMD = "---\n" +
	"kind: Service\n" +
	"name: web\n" +
	"count: 3\n" +
	"runtime:\n" +
	"  one_of: process\n" +
	"  command: /bin/http\n" +
	"ports:\n" +
	"  - name: http\n" +
	"    port: 8080\n" +
	"restart:\n" +
	"  mode: service\n" +
	"update:\n" +
	"  strategy: rolling\n" +
	"  max_surge: 1\n" +
	"health:\n" +
	"  check_type: http\n" +
	"  interval: 5s\n" +
	"---\n" +
	"# Web service\n\nServes HTTP.\n"

const invalidKindMD = "---\n" +
	"kind: CronJob\n" +
	"name: bad\n" +
	"---\nbody\n"

const serviceMissingHealthMD = "---\n" +
	"kind: Service\n" +
	"name: web\n" +
	"count: 2\n" +
	"runtime:\n" +
	"  one_of: process\n" +
	"  command: /bin/http\n" +
	"ports:\n" +
	"  - name: http\n" +
	"    port: 8080\n" +
	"restart:\n" +
	"  mode: service\n" +
	"update:\n" +
	"  strategy: rolling\n" +
	"---\n" +
	"# web\n\nbody\n"

const serviceMissingPortsMD = "---\n" +
	"kind: Service\n" +
	"name: web\n" +
	"runtime:\n" +
	"  one_of: process\n" +
	"  command: /bin/http\n" +
	"restart:\n" +
	"  mode: service\n" +
	"update:\n" +
	"  strategy: rolling\n" +
	"health:\n" +
	"  check_type: http\n" +
	"---\nbody\n"

const emptyBodyMD = "---\n" +
	"kind: Job\n" +
	"name: emptybody\n" +
	"runtime:\n" +
	"  command: /bin/true\n" +
	"---\n"

const badCELMD = "---\n" +
	"kind: Job\n" +
	"name: badcel\n" +
	"runtime:\n" +
	"  command: /bin/true\n" +
	"constraints:\n" +
	"  - 'node.role == \"web\"'\n" +
	"  - 'region == (\"us\"'\n" +
	"---\nbody\n"

func TestJobLintCmdRegistered(t *testing.T) {
	for _, c := range jobCmd.Commands() {
		if c.Name() == "lint" {
			return
		}
	}
	t.Fatal("job lint command not registered on jobCmd")
}

func TestJobLintValidSpec(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, validJobMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job lint valid: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "0 error(s)") {
		t.Errorf("expected 0 errors, got: %s", out)
	}
}

func TestJobLintValidServiceSpec(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, validServiceMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job lint valid service: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "0 error(s)") {
		t.Errorf("expected 0 errors, got: %s", out)
	}
}

func TestJobLintInvalidKind(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, invalidKindMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for invalid kind, got nil")
	}
}

func TestJobLintInvalidKindReportsError(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, invalidKindMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	_ = rootCmd.Execute()
	out := buf.String()
	if !strings.Contains(strings.ToLower(out), "cronjob") && !strings.Contains(strings.ToLower(out), "kind") {
		t.Errorf("expected error about kind, got: %s", out)
	}
}

func TestJobLintMissingRequiredField(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, serviceMissingPortsMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for missing ports, got nil")
	}
	out := buf.String()
	if !strings.Contains(strings.ToLower(out), "port") {
		t.Errorf("expected error mentioning ports, got: %s", out)
	}
}

func TestJobLintDeprecatedHCLWarning(t *testing.T) {
	resetRootFlags(t)
	spec := writeHCLSpec(t, trueJobSpec)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job lint hcl: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "migration") && !strings.Contains(strings.ToLower(out), "legacy") && !strings.Contains(strings.ToLower(out), "hcl") {
		t.Errorf("expected migration/legacy warning, got: %s", out)
	}
}

func TestJobLintExplain(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, serviceMissingHealthMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec, "--explain"})
	_ = rootCmd.Execute()
	out := buf.String()
	if !strings.Contains(out, "->") {
		t.Errorf("expected rationale lines with '->', got: %s", out)
	}
}

func TestJobLintFormatJSON(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, serviceMissingHealthMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec, "--format", "json"})
	_ = rootCmd.Execute()
	out := buf.String()
	var findings []lintFinding
	if err := json.Unmarshal(bytes.TrimSpace([]byte(out)), &findings); err != nil {
		t.Fatalf("unmarshal json findings: %v\n%s", err, out)
	}
	if len(findings) == 0 {
		t.Fatalf("expected at least one finding")
	}
	foundHealth := false
	for _, f := range findings {
		if strings.Contains(strings.ToLower(f.Message), "health") {
			foundHealth = true
		}
	}
	if !foundHealth {
		t.Errorf("expected a health-related finding, got: %+v", findings)
	}
}

func TestJobLintMissingHealthCheckWarning(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, serviceMissingHealthMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	_ = rootCmd.Execute()
	out := buf.String()
	if !strings.Contains(strings.ToLower(out), "health") {
		t.Errorf("expected health-related warning, got: %s", out)
	}
}

func TestJobLintEmptyBodyWarning(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, emptyBodyMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job lint empty body (warnings only): %v", err)
	}
	out := buf.String()
	if !strings.Contains(strings.ToLower(out), "body") {
		t.Errorf("expected body warning, got: %s", out)
	}
}

func TestJobLintBadCEL(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, badCELMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for unbalanced CEL, got nil")
	}
	out := buf.String()
	if !strings.Contains(strings.ToLower(out), "cel") && !strings.Contains(strings.ToLower(out), "parenthes") {
		t.Errorf("expected CEL/parentheses error, got: %s", out)
	}
}

func TestJobLintMissingFile(t *testing.T) {
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", "/nonexistent/spec.md"})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for missing file, got nil")
	}
}

func TestJobLintDaemonSetValid(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, "---\n"+
		"kind: DaemonSet\n"+
		"name: log-shipper\n"+
		"schedule:\n"+
		"  mode: every-node\n"+
		"restart:\n"+
		"  mode: service\n"+
		"runtime:\n"+
		"  one_of: process\n"+
		"  command: /usr/local/bin/log-shipper\n"+
		"---\n# Log shipper\n\nRuns on every node.\n")
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job lint daemonset: %v\n%s", err, buf.String())
	}
	out := buf.String()
	if !strings.Contains(out, "0 error(s)") {
		t.Errorf("expected 0 errors for valid DaemonSet, got: %s", out)
	}
}

func TestJobLintAdvisoryScheduleCron(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, "---\n"+
		"kind: Job\n"+
		"name: nightly\n"+
		"schedule:\n"+
		"  cron: \"0 2 * * *\"\n"+
		"runtime:\n"+
		"  one_of: process\n"+
		"  command: /bin/true\n"+
		"---\n# Nightly\n\nBackup.\n")
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job lint: %v\n%s", err, buf.String())
	}
	out := buf.String()
	if !strings.Contains(out, "schedule.cron' is not enforced") {
		t.Errorf("expected advisory warning for schedule.cron, got: %s", out)
	}
	if !strings.Contains(out, "0 error(s)") {
		t.Errorf("expected 0 errors, got: %s", out)
	}
}

func TestJobLintAdvisoryHealthUpdateAffinity(t *testing.T) {
	resetRootFlags(t)
	spec := writeMDSpec(t, validServiceMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "lint", spec})
	_ = rootCmd.Execute()
	out := buf.String()
	// validServiceMD has health + update blocks; both are advisory.
	if !strings.Contains(out, "field 'health' is not enforced") {
		t.Errorf("expected advisory warning for health, got: %s", out)
	}
	if !strings.Contains(out, "field 'update' is not enforced") {
		t.Errorf("expected advisory warning for update, got: %s", out)
	}
}
