package cli

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

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

type mockUpgradeRunner struct {
	calls    []mockCall
	outputs  map[string][]byte
	errs     map[string]error
	fallback []byte
}

type mockCall struct {
	name string
	args []string
}

func (m *mockUpgradeRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
	m.calls = append(m.calls, mockCall{name: name, args: append([]string(nil), args...)})
	key := name + " " + strings.Join(args, " ")
	if m.errs != nil {
		if err, ok := m.errs[key]; ok {
			return nil, err
		}
	}
	if m.outputs != nil {
		if out, ok := m.outputs[key]; ok {
			return out, nil
		}
	}
	return m.fallback, nil
}

type mockUpgradeTransport struct {
	calls []mockSSHDial
	errs  map[string]error
}

type mockSSHDial struct {
	peer string
	cmd  string
}

func (m *mockUpgradeTransport) Exec(ctx context.Context, peer string, cmd string) ([]byte, error) {
	m.calls = append(m.calls, mockSSHDial{peer: peer, cmd: cmd})
	if m.errs != nil {
		if err, ok := m.errs[peer]; ok {
			return nil, err
		}
	}
	return []byte(""), nil
}

func setupUpgradeTest(t *testing.T) {
	t.Helper()
	t.Setenv("ORCA_HOME", t.TempDir())
}

func resetUpgradeFlags() {
	upgradeTo = ""
	upgradeImportCA = false
	upgradeForce = false
	upgradeDryRun = false
	upgradeRunnerOverride = nil
	httpClientOverride = nil
	upgradeTransportOverride = nil
	peersListerOverride = nil
	migration.SetCAImporter(nil)
}

// setupUpgradeTestWithMocks calls resetRootFlags first (which resets
// all package globals including upgrade overrides), then lets the
// caller set mocks. Returns a buffer wired to rootCmd's output.
func setupUpgradeTestWithMocks(t *testing.T) *bytes.Buffer {
	t.Helper()
	t.Cleanup(resetUpgradeFlags)
	resetRootFlags(t)
	var buf bytes.Buffer
	rootCmd.SetOut(&buf)
	rootCmd.SetErr(&buf)
	return &buf
}

func TestUpgradeCmdRegistered(t *testing.T) {
	found := false
	for _, cmd := range rootCmd.Commands() {
		if cmd.Name() == "upgrade" {
			found = true
			break
		}
	}
	if !found {
		t.Fatal("upgrade command not registered on root")
	}
}

func TestUpgradeRequiresToFlag(t *testing.T) {
	setupUpgradeTest(t)
	setupUpgradeTestWithMocks(t)
	rootCmd.SetArgs([]string{"upgrade"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("upgrade without --to should fail")
	}
	if !strings.Contains(err.Error(), "--to is required") {
		t.Errorf("unexpected error: %v", err)
	}
}

func TestUpgradeDryRun(t *testing.T) {
	setupUpgradeTest(t)
	buf := setupUpgradeTestWithMocks(t)
	upgradeTo = ""
	rootCmd.SetArgs([]string{"upgrade", "--to", "v0.11.0", "--dry-run"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("upgrade dry-run: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "v0.11.0") {
		t.Errorf("output missing version: %s", out)
	}
	if !strings.Contains(out, "dry-run") {
		t.Errorf("output missing dry-run mention: %s", out)
	}
}

func TestUpgradeIdempotentSameVersion(t *testing.T) {
	setupUpgradeTest(t)
	buf := setupUpgradeTestWithMocks(t)
	saved := version
	version = "0.11.0"
	t.Cleanup(func() { version = saved })
	rootCmd.SetArgs([]string{"upgrade", "--to", "0.11.0"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("upgrade same version: %v", err)
	}
	out := buf.String()
	if !strings.Contains(out, "no-op") {
		t.Errorf("expected no-op message: %s", out)
	}
}

func TestUpgradeCutoverVerificationSuccess(t *testing.T) {
	setupUpgradeTest(t)
	setupUpgradeTestWithMocks(t)

	runner := &mockUpgradeRunner{
		outputs: map[string][]byte{
			"ss -tlnp": []byte(":443"),
		},
	}
	upgradeRunnerOverride = runner
	httpClientOverride = func(url string) (int, error) { return 200, nil }

	rootCmd.SetArgs([]string{"upgrade", "--to", "v0.11.0", "--force"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("upgrade with cutover: %v", err)
	}
}

func TestUpgradeCutoverRollback(t *testing.T) {
	setupUpgradeTest(t)
	buf := setupUpgradeTestWithMocks(t)

	runner := &mockUpgradeRunner{
		outputs: map[string][]byte{
			"ss -tlnp": []byte(":443"),
		},
	}
	upgradeRunnerOverride = runner
	httpClientOverride = func(url string) (int, error) { return 502, nil }

	rootCmd.SetArgs([]string{"upgrade", "--to", "v0.11.0"})
	err := rootCmd.Execute()
	if err == nil {
		t.Fatal("upgrade with failed cutover should return error")
	}
	if !strings.Contains(err.Error(), "cutover verification failed") && !strings.Contains(err.Error(), "rolled back") {
		t.Errorf("unexpected error: %v", err)
	}

	out := buf.String()
	if !strings.Contains(out, "Rolling back") {
		t.Errorf("output should mention rollback: %s", out)
	}

	foundRollback := false
	for _, call := range runner.calls {
		if call.name == "sed" && len(call.args) >= 2 {
			joined := strings.Join(call.args, " ")
			if strings.Contains(joined, "127.0.0.1:8443") && strings.Contains(joined, ":443") {
				foundRollback = true
			}
		}
		if call.name == "nft" && len(call.args) >= 2 && call.args[0] == "delete" {
			foundRollback = true
		}
	}
	if !foundRollback {
		t.Errorf("rollback commands not detected (calls: %v)", runner.calls)
	}
}

func TestUpgradeCutoverForceSkipsVerification(t *testing.T) {
	setupUpgradeTest(t)
	setupUpgradeTestWithMocks(t)

	runner := &mockUpgradeRunner{
		outputs: map[string][]byte{
			"ss -tlnp": []byte(":443"),
		},
	}
	upgradeRunnerOverride = runner
	verificationCalled := false
	httpClientOverride = func(url string) (int, error) {
		verificationCalled = true
		return 200, nil
	}

	rootCmd.SetArgs([]string{"upgrade", "--to", "v0.11.0", "--force"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("upgrade with --force: %v", err)
	}
	if verificationCalled {
		t.Errorf("verification should be skipped with --force")
	}
}

func TestUpgradeC27OrcaUserCreation(t *testing.T) {
	setupUpgradeTest(t)
	buf := setupUpgradeTestWithMocks(t)

	runner := &mockUpgradeRunner{}
	upgradeRunnerOverride = runner
	peersListerOverride = func() ([]string, error) {
		return []string{"peer1.example.com", "peer2.example.com"}, nil
	}
	transport := &mockUpgradeTransport{}
	upgradeTransportOverride = transport

	rootCmd.SetArgs([]string{"upgrade", "--to", "v0.11.0", "--force"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("upgrade with peer user creation: %v", err)
	}

	var useraddCalls int
	for _, call := range transport.calls {
		if strings.Contains(call.cmd, "useradd -r orca") {
			useraddCalls++
		}
	}
	if useraddCalls != 2 {
		t.Errorf("useradd called %d times, want 2 (one per peer)", useraddCalls)
	}

	out := buf.String()
	if !strings.Contains(out, "orca system user") {
		t.Errorf("output should mention orca user creation: %s", out)
	}
}

func TestUpgradeTriggersV08Migration(t *testing.T) {
	dir := t.TempDir()
	t.Setenv("ORCA_HOME", dir)
	buf := setupUpgradeTestWithMocks(t)

	createTestV08DB(t, filepath.Join(dir, "orca.db"))
	if err := os.WriteFile(filepath.Join(dir, "ca.crt"), []byte("cert"), 0o644); err != nil {
		t.Fatalf("write ca.crt: %v", err)
	}
	if err := os.WriteFile(filepath.Join(dir, "ca.key"), []byte("key"), 0o644); err != nil {
		t.Fatalf("write ca.key: %v", err)
	}

	runner := &mockUpgradeRunner{}
	upgradeRunnerOverride = runner

	rootCmd.SetArgs([]string{"upgrade", "--to", "v0.11.0", "--dry-run"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("upgrade with v0.8 layout: %v", err)
	}

	out := buf.String()
	if !strings.Contains(out, "v0.8 layout detected") {
		t.Errorf("output should mention v0.8 detection: %s", out)
	}

	migratedDB := filepath.Join(dir, paths.DefaultNamespace(), "db", "orca.db")
	if _, err := os.Stat(migratedDB); err == nil {
		t.Errorf("dry-run should not migrate the DB, but %s exists", migratedDB)
	}
}

func createTestV08DB(t *testing.T, path string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	data := fmt.Sprintf("SQLite format 3\x00")
	if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
		t.Fatalf("write db: %v", err)
	}
}

func TestUpgradeFullMigration(t *testing.T) {
	dir := t.TempDir()
	t.Setenv("ORCA_HOME", dir)
	setupUpgradeTestWithMocks(t)

	if err := os.MkdirAll(filepath.Join(dir, "cluster"), 0o755); err != nil {
		t.Fatalf("mkdir cluster: %v", err)
	}

	runner := &mockUpgradeRunner{}
	upgradeRunnerOverride = runner

	rootCmd.SetArgs([]string{"upgrade", "--to", "v0.11.0", "--force"})
	if err := rootCmd.Execute(); err != nil {
		t.Fatalf("upgrade: %v", err)
	}

	var installCalled bool
	for _, call := range runner.calls {
		if call.name == "bash" && len(call.args) > 0 && strings.Contains(call.args[0], "install.sh") {
			installCalled = true
		}
	}
	if !installCalled {
		t.Errorf("install.sh was not invoked (calls: %v)", runner.calls)
	}
}
