package cli

import (
	"bytes"
	"context"
	"encoding/json"
	"os"
	"strings"
	"testing"

	"git.cloudinit.dev/coreci/orca/internal/paths"
	"git.cloudinit.dev/coreci/orca/internal/secrets"
)

// mockVerifyTransport is a record-and-replay mock of jobVerifyTransport.
type mockVerifyTransport struct {
	writes  []mockWriteCall
	execs   []string
	execOut []byte
	execErr error
}

type mockWriteCall struct {
	peer    string
	path    string
	content []byte
	mode    os.FileMode
}

func (m *mockVerifyTransport) WriteFileIdempotent(_ context.Context, peer string, path string, content []byte, mode os.FileMode) (bool, error) {
	m.writes = append(m.writes, mockWriteCall{peer, path, content, mode})
	return true, nil
}

func (m *mockVerifyTransport) Exec(_ context.Context, _ string, cmd string) ([]byte, error) {
	m.execs = append(m.execs, cmd)
	return m.execOut, m.execErr
}

// setupVerifyEnv sets ORCA_HOME to a temp dir and writes a master key
// (txn.RenderBundle requires it).
func setupVerifyEnv(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	t.Setenv("ORCA_HOME", dir)
	mk, err := secrets.GenerateMasterKey()
	if err != nil {
		t.Fatalf("GenerateMasterKey: %v", err)
	}
	if err := secrets.SaveMasterKey(paths.MasterKeyPath(), mk); err != nil {
		t.Fatalf("SaveMasterKey: %v", err)
	}
	return dir
}

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

func TestJobVerifyValidSpec(t *testing.T) {
	setupVerifyEnv(t)
	mt := &mockVerifyTransport{execOut: []byte("verified\n")}
	jobVerifyTransportOverride = mt
	defer func() { jobVerifyTransportOverride = nil }()

	resetRootFlags(t)
	spec := writeMDSpec(t, validJobMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job verify valid: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "Planned allocations") {
		t.Errorf("expected planned allocs in output: %s", out)
	}
	if !strings.Contains(out, "systemd") && !strings.Contains(out, "unit") {
		t.Errorf("expected systemd unit in planned files: %s", out)
	}
	if len(mt.writes) == 0 {
		t.Errorf("expected bundle to be staged (writes), got 0")
	}
	if len(mt.execs) != 1 {
		t.Errorf("expected 1 exec (verify.sh), got %d", len(mt.execs))
	}
}

func TestJobVerifyInvalidSpecFails(t *testing.T) {
	setupVerifyEnv(t)
	mt := &mockVerifyTransport{}
	jobVerifyTransportOverride = mt
	defer func() { jobVerifyTransportOverride = nil }()

	resetRootFlags(t)
	spec := writeMDSpec(t, serviceMissingPortsMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for invalid spec, got nil")
	}
	if len(mt.writes) != 0 {
		t.Errorf("should not stage bundle for invalid spec, got %d writes", len(mt.writes))
	}
}

func TestJobVerifyJSONOutput(t *testing.T) {
	setupVerifyEnv(t)
	mt := &mockVerifyTransport{execOut: []byte("verified\n")}
	jobVerifyTransportOverride = mt
	defer func() { jobVerifyTransportOverride = nil }()

	resetRootFlags(t)
	spec := writeMDSpec(t, validJobMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22", "--json"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job verify --json: %v", err)
	}
	var report verifyReport
	if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &report); err != nil {
		t.Fatalf("unmarshal verify json: %v\n%s", err, buf.String())
	}
	if report.Name != "my-job" {
		t.Errorf("report.Name = %q, want my-job", report.Name)
	}
	if len(report.PlannedFiles) == 0 {
		t.Errorf("expected planned files, got 0")
	}
	if report.TxnID == "" {
		t.Errorf("expected txn id, got empty")
	}
}

func TestJobVerifyNamespaceScoping(t *testing.T) {
	setupVerifyEnv(t)
	mt := &mockVerifyTransport{execOut: []byte("verified\n")}
	jobVerifyTransportOverride = mt
	defer func() { jobVerifyTransportOverride = nil }()

	resetRootFlags(t)
	spec := writeMDSpec(t, validJobMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22", "--namespace", "prod"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job verify --namespace: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "Namespace: prod") {
		t.Errorf("expected namespace in output: %s", out)
	}
}

func TestJobVerifyNoSideEffects(t *testing.T) {
	setupVerifyEnv(t)
	mt := &mockVerifyTransport{execOut: []byte("verified\n")}
	jobVerifyTransportOverride = mt
	defer func() { jobVerifyTransportOverride = nil }()

	resetRootFlags(t)
	spec := writeMDSpec(t, validJobMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job verify: %v", err)
	}
	// Staging writes the bundle files but apply is NOT run.
	for _, w := range mt.writes {
		if strings.Contains(w.path, ".applied") {
			t.Errorf("verify staged a .applied marker (side effect): %s", w.path)
		}
	}
	for _, c := range mt.execs {
		if strings.Contains(c, "apply") {
			t.Errorf("verify ran apply (side effect): %s", c)
		}
		if strings.Contains(c, "orca-pull.sh") {
			t.Errorf("verify ran orca-pull.sh (side effect): %s", c)
		}
	}
}

func TestJobVerifyPreflightDriftReported(t *testing.T) {
	setupVerifyEnv(t)
	mt := &mockVerifyTransport{execOut: []byte("verify: drift /etc/systemd/system/orca-v1-my-job.service has drifted from last apply\n")}
	jobVerifyTransportOverride = mt
	defer func() { jobVerifyTransportOverride = nil }()

	resetRootFlags(t)
	spec := writeMDSpec(t, validJobMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"})
	// verify.sh exit 1 from drift is treated as a verify-failure by
	// the mock (execErr). But here execOut is set and execErr is nil,
	// so verify "passes" and drift is reported. We assert drift is
	// surfaced in the output and does NOT fail the dry-run.
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("job verify with drift should not fail dry-run: %v", err)
	}
	out := buf.String()
	if !strings.Contains(strings.ToLower(out), "drift") {
		t.Errorf("expected drift in output: %s", out)
	}
}

func TestJobVerifyMissingLeadFails(t *testing.T) {
	setupVerifyEnv(t)
	resetRootFlags(t)
	spec := writeMDSpec(t, validJobMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "verify", spec})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for missing --lead, got nil")
	}
}

func TestJobVerifyMissingMasterKeyFails(t *testing.T) {
	t.Setenv("ORCA_HOME", t.TempDir())
	resetRootFlags(t)
	spec := writeMDSpec(t, validJobMD)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	rootCmd.SetArgs([]string{"job", "verify", spec, "--lead", "lead:22"})
	if err := rootCmd.Execute(); err == nil {
		t.Fatal("expected error for missing master key, got nil")
	}
}
