//go:build unix

package daemon

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

	"github.com/standardbeagle/agnt/internal/protocol"
)

func TestClient_ConnectToNonExistentDaemon(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sockPath := filepath.Join(tmpDir, "test.sock")

	client := NewClient(WithSocketPath(sockPath))
	err := client.Connect()
	if err != ErrSocketNotFound {
		t.Errorf("Expected ErrSocketNotFound, got %v", err)
	}
}

func TestClient_PingPong(t *testing.T) {
	t.Parallel()
	client := newBootedClient(t)

	// Ping
	if err := client.Ping(); err != nil {
		t.Errorf("Ping failed: %v", err)
	}
}

func TestClient_Info(t *testing.T) {
	t.Parallel()
	daemon, client, _ := newBootedDaemonWithClient(t)
	sockPath := daemon.config.SocketPath

	// Get info
	info, err := client.Info()
	if err != nil {
		t.Fatalf("Info failed: %v", err)
	}

	if info.Version == "" {
		t.Error("Version should not be empty")
	}
	if info.SocketPath != sockPath {
		t.Errorf("SocketPath = %s, want %s", info.SocketPath, sockPath)
	}
}

func TestClient_Detect(t *testing.T) {
	t.Parallel()
	client := newBootedClient(t)

	// Detect project (this project is a Go project)
	result, err := client.Detect(".")
	if err != nil {
		t.Fatalf("Detect failed: %v", err)
	}

	projectType, ok := result["type"].(string)
	if !ok {
		t.Fatal("Expected type field")
	}
	// Since we're in the test directory, type should be "go"
	if projectType != "go" {
		t.Logf("Project type detected: %s", projectType)
	}
}

func TestClient_NotConnected(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	sockPath := filepath.Join(tmpDir, "nonexistent.sock")

	client := NewClient(WithSocketPath(sockPath))

	// Try to ping without daemon running - should get ErrSocketNotFound
	// (The client attempts lazy connection on first operation)
	err := client.Ping()
	if err != ErrSocketNotFound {
		t.Errorf("Expected ErrSocketNotFound, got %v", err)
	}
}

func TestClient_MultipleConnections(t *testing.T) {
	t.Parallel()
	_, sockPath := newBootedDaemon(t)

	// Create multiple clients
	clients := make([]*Client, 5)
	for i := range clients {
		clients[i] = NewClient(WithSocketPath(sockPath))
		if err := clients[i].Connect(); err != nil {
			t.Fatalf("Failed to connect client %d: %v", i, err)
		}
		defer clients[i].Close()
	}

	// All clients should be able to ping
	for i, client := range clients {
		if err := client.Ping(); err != nil {
			t.Errorf("Client %d ping failed: %v", i, err)
		}
	}
}

// TestSessionBasedCleanup verifies that when a client that registered a session
// disconnects, only resources for that session's project path are cleaned up.
func TestSessionBasedCleanup(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()

	// Create two project directories (must exist for process working directory)
	project1 := filepath.Join(tmpDir, "project1")
	project2 := filepath.Join(tmpDir, "project2")
	if err := os.MkdirAll(project1, 0755); err != nil {
		t.Fatalf("Failed to create project1 dir: %v", err)
	}
	if err := os.MkdirAll(project2, 0755); err != nil {
		t.Fatalf("Failed to create project2 dir: %v", err)
	}

	// Start daemon with zero grace period so cleanup is immediate
	sockPath := shortSockPath(t)
	daemon := newBootedDaemonWithConfig(t, DaemonConfig{
		SocketPath:         sockPath,
		CleanupGracePeriod: 1, // immediate deferred cleanup
	})
	_ = daemon

	// Client 1 - will register a session and start a process
	client1 := NewClient(WithSocketPath(sockPath))
	if err := client1.Connect(); err != nil {
		t.Fatalf("Failed to connect client1: %v", err)
	}

	// Register session for client1
	_, err := client1.SessionRegister("session1", "/tmp/overlay1", project1, "test", nil)
	if err != nil {
		t.Fatalf("Failed to register session1: %v", err)
	}

	// Start a process for project1 via Run (using raw mode)
	_, err = client1.Run(protocol.RunConfig{
		ID:      "proc1",
		Path:    project1,
		Mode:    "background",
		Command: "sleep",
		Args:    []string{"100"},
		Raw:     true,
	})
	if err != nil {
		t.Fatalf("Failed to start process1: %v", err)
	}

	// Client 2 - will NOT register a session but start a process for project2
	client2 := NewClient(WithSocketPath(sockPath))
	if err := client2.Connect(); err != nil {
		t.Fatalf("Failed to connect client2: %v", err)
	}
	defer client2.Close()

	// Start a process for project2 (without a session, using raw mode)
	_, err = client2.Run(protocol.RunConfig{
		ID:      "proc2",
		Path:    project2,
		Mode:    "background",
		Command: "sleep",
		Args:    []string{"100"},
		Raw:     true,
	})
	if err != nil {
		t.Fatalf("Failed to start process2: %v", err)
	}

	// Verify both processes are running
	procs, err := client2.ProcList(protocol.DirectoryFilter{Global: true})
	if err != nil {
		t.Fatalf("Failed to list processes: %v", err)
	}
	procsList, ok := procs["processes"].([]interface{})
	if !ok {
		t.Fatalf("Expected processes list, got %T", procs["processes"])
	}
	if len(procsList) != 2 {
		t.Fatalf("Expected 2 processes, got %d", len(procsList))
	}

	// Close client1 (should trigger cleanup for project1 only)
	client1.Close()

	// Poll until cleanup completes. The daemon runs cleanup via a deferred
	// timer (CleanupGracePeriod=1ns), so we must not assert before the
	// goroutine has observed the disconnect — but we also must not pin the
	// test to a fixed wall-clock wait.
	deadline := time.Now().Add(5 * time.Second)
	for {
		procs, err = client2.ProcList(protocol.DirectoryFilter{Global: true})
		if err != nil {
			t.Fatalf("Failed to list processes after cleanup: %v", err)
		}
		procsList, ok = procs["processes"].([]interface{})
		if !ok {
			t.Fatalf("Expected processes list after cleanup, got %T", procs["processes"])
		}
		if len(procsList) == 1 {
			break
		}
		if time.Now().After(deadline) {
			t.Fatalf("Expected 1 process after cleanup within 5s, still have %d", len(procsList))
		}
		time.Sleep(25 * time.Millisecond)
	}

	if len(procsList) > 0 {
		proc := procsList[0].(map[string]interface{})
		if proc["id"] != "proc2" {
			t.Errorf("Expected proc2 to survive, got %v", proc["id"])
		}
	}

	// Verify session1 is unregistered
	sessionsResult, err := client2.SessionList(protocol.DirectoryFilter{Global: true})
	if err != nil {
		t.Fatalf("Failed to list sessions: %v", err)
	}
	sessionsList, _ := sessionsResult["sessions"].([]interface{})
	for _, s := range sessionsList {
		session := s.(map[string]interface{})
		if session["code"] == "session1" {
			t.Error("session1 should have been unregistered")
		}
	}
}

func TestClient_SessionSchedule(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	client := newBootedClient(t)

	// Register a session first
	projectPath := filepath.Join(tmpDir, "project")
	if err := os.MkdirAll(projectPath, 0755); err != nil {
		t.Fatalf("Failed to create project dir: %v", err)
	}

	_, err := client.SessionRegister("sched-session", "/tmp/overlay", projectPath, "test", nil)
	if err != nil {
		t.Fatalf("Failed to register session: %v", err)
	}

	// Schedule a message
	result, err := client.SessionSchedule("sched-session", "1h", "test scheduled message")
	if err != nil {
		t.Fatalf("SessionSchedule failed: %v", err)
	}

	if result == nil {
		t.Fatal("Expected non-nil result from SessionSchedule")
	}

	taskID, ok := result["task_id"].(string)
	if !ok || taskID == "" {
		t.Error("Expected task_id in result")
	}
}

func TestClient_SessionCancel(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	client := newBootedClient(t)

	// Register a session first
	projectPath := filepath.Join(tmpDir, "project")
	if err := os.MkdirAll(projectPath, 0755); err != nil {
		t.Fatalf("Failed to create project dir: %v", err)
	}

	_, err := client.SessionRegister("cancel-session", "/tmp/overlay", projectPath, "test", nil)
	if err != nil {
		t.Fatalf("Failed to register session: %v", err)
	}

	// Schedule a message
	schedResult, err := client.SessionSchedule("cancel-session", "1h", "test scheduled message")
	if err != nil {
		t.Fatalf("SessionSchedule failed: %v", err)
	}

	taskID := schedResult["task_id"].(string)

	// Cancel the scheduled task
	err = client.SessionCancel(taskID)
	if err != nil {
		t.Fatalf("SessionCancel failed: %v", err)
	}

	// Verify task is no longer present
	tasksResult, err := client.SessionTasks(protocol.DirectoryFilter{Global: true})
	if err != nil {
		t.Fatalf("Failed to list tasks: %v", err)
	}

	tasks, _ := tasksResult["tasks"].([]interface{})
	for _, task := range tasks {
		taskMap := task.(map[string]interface{})
		if taskMap["id"] == taskID {
			t.Error("Cancelled task should not be in list")
		}
	}
}

func TestClient_SessionAttach(t *testing.T) {
	t.Parallel()
	tmpDir := t.TempDir()
	client := newBootedClient(t)

	// Register a session in a nested directory
	projectPath := filepath.Join(tmpDir, "project", "src")
	if err := os.MkdirAll(projectPath, 0755); err != nil {
		t.Fatalf("Failed to create project dir: %v", err)
	}

	_, err := client.SessionRegister("attach-session", "/tmp/overlay", projectPath, "test", nil)
	if err != nil {
		t.Fatalf("Failed to register session: %v", err)
	}

	// Attach from the same directory
	result, err := client.SessionAttach(projectPath)
	if err != nil {
		t.Fatalf("SessionAttach failed: %v", err)
	}

	if result == nil {
		t.Fatal("Expected non-nil result from SessionAttach")
	}

	// Result should contain session info - check the nested session object
	session, ok := result["session"].(map[string]interface{})
	if ok {
		code := session["code"].(string)
		if code != "attach-session" {
			t.Errorf("Expected code 'attach-session', got %q", code)
		}
	} else if code, ok := result["code"].(string); ok {
		if code != "attach-session" {
			t.Errorf("Expected code 'attach-session', got %q", code)
		}
	} else {
		// Log actual result for debugging
		t.Logf("SessionAttach result: %v", result)
	}
}

func TestClient_TunnelStart(t *testing.T) {
	t.Parallel()
	client := newBootedClient(t)

	// Try to start a tunnel (will fail if cloudflared not installed, but should exercise the code)
	_, err := client.TunnelStart(protocol.TunnelStartConfig{
		ID:        "test-tunnel",
		Provider:  "cloudflare",
		LocalPort: 8080,
	})

	// We expect either success or an error about cloudflared not being available
	// The important thing is that the client method is exercised
	if err != nil {
		// Check if it's an expected error about missing binary
		t.Logf("TunnelStart returned expected error: %v", err)
	}
}
