package cli

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"testing"
	"time"

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

// initTestEnv sets ORCA_HOME to a temp dir and returns a cleanup func.
func initTestEnv(t *testing.T) (string, func()) {
	t.Helper()
	dir := t.TempDir()
	orig := os.Getenv("ORCA_HOME")
	if err := os.Setenv("ORCA_HOME", dir); err != nil {
		t.Fatalf("set ORCA_HOME: %v", err)
	}
	return dir, func() {
		if err := os.Setenv("ORCA_HOME", orig); err != nil {
			t.Fatalf("restore ORCA_HOME: %v", err)
		}
	}
}

// discardWriter is an io.Writer that discards all output (for tests
// that don't need to inspect init stdout).
type discardWriter struct{}

func (discardWriter) Write(p []byte) (int, error) { return len(p), nil }

var _ io.Writer = discardWriter{}

func TestInit_FullBootstrap(t *testing.T) {
	dir, cleanup := initTestEnv(t)
	defer cleanup()

	if err := runInit(discardWriter{}); err != nil {
		t.Fatalf("init: %v", err)
	}

	// Verify namespace dir exists.
	if _, err := os.Stat(dir); err != nil {
		t.Errorf("namespace dir missing: %v", err)
	}

	// Verify CA files exist with correct modes.
	caCert := certpaths.CACertPath()
	caKey := certpaths.CAKeyPath()
	if _, err := os.Stat(caCert); err != nil {
		t.Errorf("ca.crt missing: %v", err)
	}
	if info, err := os.Stat(caKey); err == nil {
		if info.Mode().Perm() != 0o600 {
			t.Errorf("ca.key mode = %04o, want 0600", info.Mode().Perm())
		}
	} else {
		t.Errorf("ca.key missing: %v", err)
	}

	// Verify server cert exists.
	if _, err := os.Stat(certpaths.ServerCertPath()); err != nil {
		t.Errorf("server.crt missing: %v", err)
	}

	// Verify DB exists and has migrations applied.
	db, err := store.Open(certpaths.DBPath())
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	defer db.Close()

	ctx := context.Background()
	version, err := store.MigrationVersion(ctx, db)
	if err != nil {
		t.Fatalf("migration version: %v", err)
	}
	if version != "0008_audit_tamper_evidence.sql" {
		t.Errorf("migration version = %q, want 0008_audit_tamper_evidence.sql", version)
	}

	// Verify localhost node registered with kind=localhost.
	repo := store.NewNodeRepo(db)
	node, err := repo.GetByName(ctx, "localhost")
	if err != nil {
		t.Fatalf("get localhost node: %v", err)
	}
	if node.Kind != string(model.NodeKindLocalhost) {
		t.Errorf("node kind = %q, want localhost", node.Kind)
	}
	if node.OS == "" {
		t.Errorf("node os is empty, expected detected value")
	}
	if node.Address != "localhost:8443" {
		t.Errorf("node address = %q, want localhost:8443", node.Address)
	}
}

func TestInit_IdempotentReRun(t *testing.T) {
	_, cleanup := initTestEnv(t)
	defer cleanup()

	// First init.
	if err := runInit(discardWriter{}); err != nil {
		t.Fatalf("first init: %v", err)
	}

	// Capture first-run state.
	caCertBefore, _ := os.ReadFile(certpaths.CACertPath())
	serverCertBefore, _ := os.ReadFile(certpaths.ServerCertPath())

	db, err := store.Open(certpaths.DBPath())
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	repo := store.NewNodeRepo(db)
	ctx := context.Background()
	nodeBefore, err := repo.GetByName(ctx, "localhost")
	if err != nil {
		t.Fatalf("get node before: %v", err)
	}
	nodeIDBefore := nodeBefore.ID
	joinedAtBefore := nodeBefore.JoinedAt
	if err := db.Close(); err != nil {
		t.Fatalf("close db: %v", err)
	}

	// Wait a moment so last_seen can differ.
	time.Sleep(50 * time.Millisecond)

	// Second init (should be idempotent).
	if err := runInit(discardWriter{}); err != nil {
		t.Fatalf("second init: %v", err)
	}

	// CA and server cert must NOT have been regenerated.
	caCertAfter, _ := os.ReadFile(certpaths.CACertPath())
	serverCertAfter, _ := os.ReadFile(certpaths.ServerCertPath())
	if string(caCertBefore) != string(caCertAfter) {
		t.Error("CA was regenerated on re-run (D-036 violation)")
	}
	if string(serverCertBefore) != string(serverCertAfter) {
		t.Error("server cert was regenerated on re-run (D-036 violation)")
	}

	// Node ID and joined_at must be unchanged; last_seen should be refreshed.
	db, err = store.Open(certpaths.DBPath())
	if err != nil {
		t.Fatalf("reopen db: %v", err)
	}
	defer db.Close()
	repo = store.NewNodeRepo(db)
	nodeAfter, err := repo.GetByName(ctx, "localhost")
	if err != nil {
		t.Fatalf("get node after: %v", err)
	}
	if nodeAfter.ID != nodeIDBefore {
		t.Errorf("node id changed: was %s, now %s (D-036 violation)", nodeIDBefore, nodeAfter.ID)
	}
	if !nodeAfter.JoinedAt.Equal(joinedAtBefore) {
		t.Errorf("joined_at changed: was %v, now %v (D-036 violation)", joinedAtBefore, nodeAfter.JoinedAt)
	}
	if !nodeAfter.LastSeen.After(joinedAtBefore) {
		t.Errorf("last_seen not refreshed: was %v, now %v", joinedAtBefore, nodeAfter.LastSeen)
	}

	// No duplicate localhost nodes.
	nodes, err := repo.List(ctx)
	if err != nil {
		t.Fatalf("list nodes: %v", err)
	}
	localhostCount := 0
	for _, n := range nodes {
		if n.Name == "localhost" {
			localhostCount++
		}
	}
	if localhostCount != 1 {
		t.Errorf("found %d localhost nodes, want 1 (idempotency)", localhostCount)
	}
}

func TestInit_NamespaceDirCreation(t *testing.T) {
	dir, cleanup := initTestEnv(t)
	defer cleanup()

	// The namespace dir is the ORCA_HOME temp dir itself — but let's
	// point at a non-existent subdir to test MkdirAll.
	subDir := filepath.Join(dir, "nested", "orca-state")
	if err := os.Setenv("ORCA_HOME", subDir); err != nil {
		t.Fatalf("set ORCA_HOME: %v", err)
	}

	if err := runInit(discardWriter{}); err != nil {
		t.Fatalf("init with nested dir: %v", err)
	}
	if _, err := os.Stat(subDir); err != nil {
		t.Errorf("nested namespace dir not created: %v", err)
	}
}
