package main

import (
	"encoding/base64"
	"encoding/json"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
)

// setupTestEnv crea un dir temporal, redirige approvalQueuePath alli, y devuelve cleanup.
func setupTestEnv(t *testing.T) (string, func()) {
	t.Helper()
	tmp, err := os.MkdirTemp("", "device_agent_test_*")
	if err != nil {
		t.Fatalf("mkdtemp: %v", err)
	}
	prevWD, _ := os.Getwd()
	if err := os.Chdir(tmp); err != nil {
		_ = os.RemoveAll(tmp)
		t.Fatalf("chdir: %v", err)
	}
	prevApprovalPath := approvalQueuePath
	approvalQueuePath = filepath.Join(tmp, "local_files", "approval_queue.jsonl")
	cleanup := func() {
		approvalQueuePath = prevApprovalPath
		_ = os.Chdir(prevWD)
		_ = os.RemoveAll(tmp)
	}
	return tmp, cleanup
}

// TestShellEval_BlocklistRmRf comprueba que rm -rf / es rechazado sin ejecucion.
func TestShellEval_BlocklistRmRf(t *testing.T) {
	_, cleanup := setupTestEnv(t)
	defer cleanup()
	cap := &Capability{Name: "shell.eval"}
	_, _, _, err := runShellEval(cap, map[string]any{"cmd": "rm -rf /"})
	if err == nil {
		t.Fatal("expected error from blocklist, got nil")
	}
	if !strings.Contains(err.Error(), "blocked") {
		t.Fatalf("expected 'blocked' in err, got: %v", err)
	}
}

// TestShellEval_BlocklistCaseInsensitive comprueba que case-insensitive funciona.
func TestShellEval_BlocklistCaseInsensitive(t *testing.T) {
	_, cleanup := setupTestEnv(t)
	defer cleanup()
	cap := &Capability{Name: "shell.eval"}
	_, _, _, err := runShellEval(cap, map[string]any{"cmd": "RM -RF /"})
	if err == nil {
		t.Fatal("expected error from blocklist (case insensitive), got nil")
	}
	if !strings.Contains(err.Error(), "blocked") {
		t.Fatalf("expected 'blocked' in err, got: %v", err)
	}
}

// TestShellEval_AutoApproveGitStatus comprueba que git status entra en defaultAutoApprove.
func TestShellEval_AutoApproveGitStatus(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("bash test on windows skipped")
	}
	_, cleanup := setupTestEnv(t)
	defer cleanup()
	cap := &Capability{Name: "shell.eval"}
	res, exit, extra, err := runShellEval(cap, map[string]any{"cmd": "git status --porcelain"})
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}
	if res["approval_status"] != "auto-approved" {
		t.Fatalf("expected approval_status=auto-approved, got: %v", res["approval_status"])
	}
	// git might exit non-zero if not in a repo, but it should have *executed*
	if extra == nil {
		t.Fatal("expected extra audit record, got nil")
	}
	if extra.Cmd != "git status --porcelain" {
		t.Fatalf("extra.Cmd mismatch: %q", extra.Cmd)
	}
	_ = exit
}

// TestShellEval_QueuedWhenRequiresApproval comprueba que un cmd no auto + requires_approval = queued + error.
func TestShellEval_QueuedWhenRequiresApproval(t *testing.T) {
	tmp, cleanup := setupTestEnv(t)
	defer cleanup()

	cap := &Capability{
		Name:             "shell.eval",
		RequiresApproval: true,
		AutoApprove:      []string{`^echo\s`}, // override defaults — solo echo entra
	}
	_, _, _, err := runShellEval(cap, map[string]any{"cmd": "ls -la"})
	if err == nil {
		t.Fatal("expected approval_required error, got nil")
	}
	if !strings.Contains(err.Error(), "approval_required") {
		t.Fatalf("expected 'approval_required' in err, got: %v", err)
	}
	// queue file existe + contiene la entry
	qPath := filepath.Join(tmp, "local_files", "approval_queue.jsonl")
	data, ferr := os.ReadFile(qPath)
	if ferr != nil {
		t.Fatalf("expected approval queue file, got: %v", ferr)
	}
	if !strings.Contains(string(data), `"cmd":"ls -la"`) {
		t.Fatalf("approval queue missing cmd, got: %s", data)
	}
	if !strings.Contains(string(data), `"status":"pending"`) {
		t.Fatalf("approval queue missing status=pending, got: %s", data)
	}
}

// TestShellEval_NoApprovalNeededExecutes: cmd no auto + no requires_approval -> ejecuta directo.
func TestShellEval_NoApprovalNeededExecutes(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("bash test on windows skipped")
	}
	_, cleanup := setupTestEnv(t)
	defer cleanup()
	cap := &Capability{
		Name:             "shell.eval",
		RequiresApproval: false,
		AutoApprove:      []string{`^echo\s`}, // strict autoapprove (irrelevant since no approval needed)
	}
	res, _, extra, err := runShellEval(cap, map[string]any{"cmd": "printf hello"})
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}
	if res["approval_status"] != "none-required" {
		t.Fatalf("expected approval_status=none-required, got: %v", res["approval_status"])
	}
	if extra.Stdout != "hello" {
		t.Fatalf("expected stdout 'hello', got: %q", extra.Stdout)
	}
}

// TestShellEval_Timeout: sleep 5 con TimeoutSeconds=1 -> exit_code != 0.
func TestShellEval_Timeout(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("bash test on windows skipped")
	}
	_, cleanup := setupTestEnv(t)
	defer cleanup()
	cap := &Capability{
		Name:           "shell.eval",
		TimeoutSeconds: 1,
		AutoApprove:    []string{`^sleep\s`}, // autoapprove sleep for the test
	}
	res, _, _, err := runShellEval(cap, map[string]any{"cmd": "sleep 5"})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	exit, _ := res["exit_code"].(int)
	if exit == 0 {
		t.Fatalf("expected non-zero exit on timeout, got 0; result: %+v", res)
	}
}

// TestShellEval_OutputTruncation: output > MaxOutputBytes -> truncated=true.
func TestShellEval_OutputTruncation(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("bash test on windows skipped")
	}
	_, cleanup := setupTestEnv(t)
	defer cleanup()
	cap := &Capability{
		Name:           "shell.eval",
		MaxOutputBytes: 50,
		AutoApprove:    []string{`^printf`},
	}
	// produce > 50 bytes
	res, _, _, err := runShellEval(cap, map[string]any{
		"cmd": `printf '%0.s-' {1..500}`,
	})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if res["truncated"] != true {
		t.Fatalf("expected truncated=true, got: %v (full result %+v)", res["truncated"], res)
	}
}

// TestShellEval_PowershellOnLinuxWithoutPwsh: si pwsh no esta en PATH y se pide powershell, error.
func TestShellEval_PowershellOnLinuxWithoutPwsh(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test only meaningful on non-windows")
	}
	// Save PATH and clear it to ensure pwsh not found
	origPATH := os.Getenv("PATH")
	defer os.Setenv("PATH", origPATH)
	os.Setenv("PATH", "/nonexistent")

	_, cleanup := setupTestEnv(t)
	defer cleanup()
	cap := &Capability{Name: "shell.eval"}
	_, _, _, err := runShellEval(cap, map[string]any{
		"cmd":   "echo hi",
		"shell": "powershell",
	})
	if err == nil {
		t.Fatal("expected error when powershell requested on linux without pwsh")
	}
	if !strings.Contains(err.Error(), "powershell") && !strings.Contains(err.Error(), "pwsh") {
		t.Fatalf("err msg doesn't mention powershell/pwsh: %v", err)
	}
}

// TestShellEval_AuditVerboseWritesCleartext: tras una exec, audit_shell_eval tiene row con cmd en claro.
func TestShellEval_AuditVerboseWritesCleartext(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("bash test on windows skipped")
	}
	tmp, cleanup := setupTestEnv(t)
	defer cleanup()

	auditPath := filepath.Join(tmp, "audit.db")
	audit, err := OpenAudit(auditPath)
	if err != nil {
		t.Fatalf("open audit: %v", err)
	}
	defer audit.Close()

	cap := &Capability{
		Name:        "shell.eval",
		AutoApprove: []string{`^printf`},
	}
	_, exitCode, extra, err := runShellEval(cap, map[string]any{"cmd": "printf forensic-cleartext"})
	if err != nil {
		t.Fatalf("runShellEval: %v", err)
	}
	if extra == nil {
		t.Fatal("expected extra record")
	}
	_, err = audit.AppendVerbose("req-1", "shell.eval", []string{"printf forensic-cleartext"}, exitCode, *extra)
	if err != nil {
		t.Fatalf("AppendVerbose: %v", err)
	}

	// Read back
	rows, err := audit.db.Query(`SELECT cmd, shell, stdout_b64 FROM audit_shell_eval`)
	if err != nil {
		t.Fatalf("query: %v", err)
	}
	defer rows.Close()
	found := false
	for rows.Next() {
		var cmd, shell, stdoutB64 string
		if err := rows.Scan(&cmd, &shell, &stdoutB64); err != nil {
			t.Fatalf("scan: %v", err)
		}
		if cmd != "printf forensic-cleartext" {
			t.Fatalf("expected cleartext cmd, got: %q", cmd)
		}
		if !strings.HasPrefix(stdoutB64, "plain:") {
			t.Fatalf("expected plain-base64 stdout (short), got: %q", stdoutB64)
		}
		// Decode + verify
		raw, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(stdoutB64, "plain:"))
		if string(raw) != "forensic-cleartext" {
			t.Fatalf("decoded stdout mismatch: %q", raw)
		}
		_ = shell
		found = true
	}
	if !found {
		t.Fatal("expected at least one row in audit_shell_eval")
	}
}

// TestShellEval_ParseArgsJSONObject roundtrip.
func TestShellEval_ParseArgsJSONObject(t *testing.T) {
	jsonStr, _ := json.Marshal(map[string]any{"cmd": "ls", "cwd": "/tmp"})
	m, err := parseShellEvalArgs([]string{string(jsonStr)})
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if m["cmd"] != "ls" || m["cwd"] != "/tmp" {
		t.Fatalf("unexpected parsed: %+v", m)
	}
}

// TestShellEval_ParseArgsPositional fallback.
func TestShellEval_ParseArgsPositional(t *testing.T) {
	m, err := parseShellEvalArgs([]string{"ls -la", "/tmp", "bash"})
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if m["cmd"] != "ls -la" || m["cwd"] != "/tmp" || m["shell"] != "bash" {
		t.Fatalf("unexpected parsed: %+v", m)
	}
}
