package cli

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

	"git.cloudinit.dev/coreci/orca/internal/certpaths"

	"git.cloudinit.dev/coreci/orca/internal/migration"

	"github.com/spf13/cobra"
)

func resetRootFlags(t *testing.T) {
	t.Helper()
	rootCmd.SetArgs(nil)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	_ = rootCmd.PersistentFlags().Set("system", "false")
	_ = rootCmd.PersistentFlags().Set("json", "false")
	_ = rootCmd.PersistentFlags().Set("no-deprecation-warnings", "false")
	resetCommandFlags()
}

// resetCommandFlags zeroes the package-level flag-bound vars used by
// individual subcommands so tests don't leak state between runs (cobra
// parses into these globals; without a reset a prior test's value
// persists). resetRootFlags calls this; tests that exercise a single
// command without resetRootFlags may call it directly.
func resetCommandFlags() {
	joinName, joinAddr, joinCAFinger, joinType = "", "", "", "localhost"
	joinHost, joinSSHUser, joinSSHKey, proxmoxUser, proxmoxRole = "", "root", "", "orca", "OrcaOperator"
	joinSSHPort, leaveID, nodeWatch = 22, "", false
	stopID, runTarget, runIDKey, jobWatch = "", "", "", false
	migrateTarget = ""
	drainTimeout = 30 * time.Second
	logsAllNodes, logsNode, logsJob, logsSince, logsJSON = false, "", "", "5m", false
	capSetCPU, capSetMem, capSetDisk, capNodeID = 0, 0, 0, ""
	auditLimit = 50
	backupOutPath, restoreInPath, restoreTargetDir = "", "", ""
	restoreForce = false
	restoreDryRun = false
	collectorRoot = "/"
	collectorDryRun = false
	txnApplyForce, txnApplyAckRisk, txnApplyYes = false, false, false
	txnApplyNamespace = ""
	txnApplyTimeout = 5 * time.Minute
	txnApplyLead, txnRollbackLead = "", ""
	driftWatchInterval = 2 * time.Second
	driftWatchPaths = nil
	driftShowPeer = ""
	driftConfigPath = ""
	driftRemediateForce = false
	jobRestartPeer = ""
	jobLintExplain = false
	jobLintFormat = "text"
	jobVerifyLead = ""
	jobVerifyNamespace = ""
	jobVerifyJSON = false
	peerSetupNoOrcaUser = false
	upgradeTo = ""
	upgradeImportCA = false
	upgradeForce = false
	upgradeDryRun = false
	upgradeRunnerOverride = nil
	httpClientOverride = nil
	upgradeTransportOverride = nil
	peersListerOverride = nil
	migration.SetCAImporter(nil)
	cutoverTimeout = 5 * time.Minute
	rotateLeadTo = ""
	rotateLeadForce = false
	resetNSFlags()
	// Reset per-command output writers so tests that polluted them
	// (e.g. daemon tests calling cmd.SetOut(&buf)) don't leak into
	// other tests. nil → cobra walks to rootCmd's writer.
	for _, c := range []*cobra.Command{
		nodeDrainCmd, daemonCmd, daemonDrainAndStopCmd,
		jobCmd, jobMigrateCmd, jobRunCmd, jobListCmd, jobStopCmd, jobLogsCmd, jobLintCmd, jobVerifyCmd,
		logsCmd,
		clusterCmd, clusterCutoverCmd, clusterRotateLeadCmd, compatCheckCmd, noOrcaOnServerCmd,
	} {
		if c != nil {
			c.SetOut(nil)
			c.SetErr(nil)
		}
	}
}

func TestNamespaceDefaultsToUserHome(t *testing.T) {
	t.Setenv("ORCA_HOME", "")
	home, err := os.UserHomeDir()
	if err != nil {
		t.Fatalf("UserHomeDir: %v", err)
	}
	want := filepath.Join(home, ".orca")
	if got := certpaths.Dir(); got != want {
		t.Errorf("certpaths.Dir() = %q, want %q", got, want)
	}
}

func TestNamespaceHonorsORCAHOME(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("ORCA_HOME", tmp)
	if got := certpaths.Dir(); got != tmp {
		t.Errorf("certpaths.Dir() = %q, want %q", got, tmp)
	}
	if got := certpaths.DBPath(); got != filepath.Join(tmp, "orca.db") {
		t.Errorf("certpaths.DBPath() = %q, want %q", got, filepath.Join(tmp, "orca.db"))
	}
}

func TestInitHonorsORCAHOME(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("ORCA_HOME", tmp)
	resetRootFlags(t)

	rootCmd.SetArgs([]string{"init"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("init: %v", err)
	}

	info, err := os.Stat(tmp)
	if err != nil {
		t.Fatalf("stat %s: %v", tmp, err)
	}
	if !info.IsDir() {
		t.Errorf("%s is not a directory", tmp)
	}
}

func TestSystemFlagSetsORCAHOME(t *testing.T) {
	t.Setenv("ORCA_HOME", "")
	resetRootFlags(t)

	rootCmd.SetArgs([]string{"--system", "init"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("--system init: %v", err)
	}
	if got := os.Getenv("ORCA_HOME"); got != systemNamespaceRoot {
		t.Errorf("ORCA_HOME = %q, want %q", got, systemNamespaceRoot)
	}
}

func TestSystemFlagConflictsWithORCAHOME(t *testing.T) {
	t.Setenv("ORCA_HOME", "/custom/path")
	resetRootFlags(t)

	rootCmd.SetArgs([]string{"--system", "init"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("expected error for --system + ORCA_HOME conflict, got nil")
	}
}

func TestInitJSONOutput(t *testing.T) {
	tmp := t.TempDir()
	t.Setenv("ORCA_HOME", tmp)
	resetRootFlags(t)

	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetArgs([]string{"init", "--json"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("init --json: %v", err)
	}

	// v0.6: init --json now outputs a full bootstrap summary object.
	var result map[string]any
	if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
		t.Fatalf("unmarshal init output: %v\noutput: %s", err, buf.String())
	}
	if result["namespace"] != tmp {
		t.Errorf("init --json namespace = %q, want %q", result["namespace"], tmp)
	}
	if result["os"] == nil || result["os"] == "" {
		t.Errorf("init --json os is missing/empty")
	}
	if result["node_id"] == nil || result["node_id"] == "" {
		t.Errorf("init --json node_id is missing/empty")
	}
	steps, ok := result["steps"].([]any)
	if !ok || len(steps) < 6 {
		t.Errorf("init --json steps: expected 6+ entries, got %v", result["steps"])
	}
}

func TestSystemFlagIsPersistent(t *testing.T) {
	for _, name := range []string{"system", "json"} {
		f := rootCmd.PersistentFlags().Lookup(name)
		if f == nil {
			t.Errorf("persistent flag %q not found", name)
		}
	}
}
