package cli

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

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

func writeJobSpec(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 trueJobSpec = `job "true" {}
task "t" {
  command = "/bin/true"
}
`

const falseJobSpec = `job "false" {}
task "t" {
  command = "/bin/false"
}
`

func TestJobRunComplete(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	spec := writeJobSpec(t, trueJobSpec)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "run", spec})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job run: %v", err)
	}
	if !strings.Contains(buf.String(), "Job complete") {
		t.Errorf("job run output unexpected: %s", buf.String())
	}
}

func TestJobRunCompleteJSON(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	spec := writeJobSpec(t, trueJobSpec)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "run", spec, "--json"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job run --json: %v", err)
	}
	var result map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
		t.Fatalf("unmarshal job run json: %v\n%s", err, buf.String())
	}
	if result["status"] != "complete" {
		t.Errorf("job run --json status = %v, want complete", result["status"])
	}
}

func TestJobRunFailed(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	spec := writeJobSpec(t, falseJobSpec)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "run", spec})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for failing job, got nil")
	}
}

func TestJobRunFailedJSON(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	spec := writeJobSpec(t, falseJobSpec)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "run", spec, "--json"})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for failing job --json, got nil")
	}
	if !strings.Contains(buf.String(), "failed") {
		t.Errorf("job run --json failed output unexpected: %s", buf.String())
	}
}

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

func TestJobListEmpty(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "list"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job list: %v", err)
	}
	if !strings.Contains(buf.String(), "No jobs") {
		t.Errorf("job list empty output unexpected: %s", buf.String())
	}
}

func TestJobListJSONEmpty(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "list", "--json"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job list --json: %v", err)
	}
	var jobs []map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &jobs); err != nil {
		t.Fatalf("unmarshal job list json: %v\n%s", err, buf.String())
	}
	if len(jobs) != 0 {
		t.Errorf("job list --json empty = %d jobs, want 0", len(jobs))
	}
}

func TestJobListAfterRun(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	spec := writeJobSpec(t, trueJobSpec)
	resetRootFlags(t)
	rootCmd.SetArgs([]string{"job", "run", spec})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job run: %v", err)
	}
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "list"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job list: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "true") {
		t.Errorf("job list missing job name: %s", out)
	}
}

func TestJobStop(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	jobID := seedJob(t, "stopper", model.JobStatusRunning)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "stop", jobID})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job stop: %v", err)
	}
	if !strings.Contains(buf.String(), "Job stopped") {
		t.Errorf("job stop output unexpected: %s", buf.String())
	}
}

func TestJobStopJSON(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	jobID := seedJob(t, "jsonstopper", model.JobStatusRunning)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "stop", jobID, "--json"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job stop --json: %v", err)
	}
	var result map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
		t.Fatalf("unmarshal job stop json: %v\n%s", err, buf.String())
	}
	if result["status"] != "stopped" {
		t.Errorf("job stop --json status = %v, want stopped", result["status"])
	}
}

func TestJobStopNotFound(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "stop", "nonexistent-id"})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for job stop not found, got nil")
	}
}

func TestJobStopMissingID(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "stop"})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for job stop without id, got nil")
	}
}

func TestJobLogsEmpty(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	jobID := seedJob(t, "logger", model.JobStatusComplete)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "logs", jobID})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job logs: %v", err)
	}
	if !strings.Contains(buf.String(), "No tasks") {
		t.Errorf("job logs empty output unexpected: %s", buf.String())
	}
}

func TestJobLogsJSONEmpty(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	jobID := seedJob(t, "jsonlogger", model.JobStatusComplete)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "logs", jobID, "--json"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job logs --json: %v", err)
	}
	var tasks []map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &tasks); err != nil {
		t.Fatalf("unmarshal job logs json: %v\n%s", err, buf.String())
	}
	if len(tasks) != 0 {
		t.Errorf("job logs --json empty = %d tasks, want 0", len(tasks))
	}
}

func TestJobLogsMissingID(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "logs"})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for job logs without id, got nil")
	}
}

func seedJob(t *testing.T, name string, status model.JobStatus) string {
	t.Helper()
	db, err := store.Open(certpaths.DBPath())
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	defer db.Close()
	repo := store.NewJobRepo(db)
	j := &model.Job{
		ID:     "job-" + name,
		Name:   name,
		Spec:   "spec.hcl",
		Status: status,
	}
	if err := repo.Insert(t.Context(), j); err != nil {
		t.Fatalf("insert job: %v", err)
	}
	return j.ID
}
