package ipc

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"os"
	"strings"
	"sync"
	"testing"
	"time"
)

// TestNewManager tests IPC manager creation
func TestNewManager(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-manager-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	if m == nil {
		t.Fatal("NewManager returned nil")
	}

	if m.sockDir != tmpDir {
		t.Errorf("Expected sockDir %s, got %s", tmpDir, m.sockDir)
	}
}

// TestCreateSession tests creating an IPC session
func TestCreateSession(t *testing.T) {
	// Use /tmp directly to avoid long paths (Unix socket path limit ~104 bytes)
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	sessionID := "test-session-1"
	agentCtx := &AgentContext{
		UserID: "user1",
		ChatID: "chat1",
		Locale: "en-US",
	}

	mcpTools := map[string]*MCPTool{
		"test_tool": {
			Name:        "test_tool",
			Description: "A test tool",
			Process:     "scripts.test.hello",
			InputSchema: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}}}`),
		},
	}

	session, err := m.Create(ctx, sessionID, agentCtx, mcpTools)
	if err != nil {
		t.Fatalf("Create session failed: %v", err)
	}
	defer m.Close(sessionID)

	// Verify session properties
	if session.ID != sessionID {
		t.Errorf("Expected session ID %s, got %s", sessionID, session.ID)
	}

	// Socket path uses hash now, just verify it's in the right directory and ends with .sock
	if !strings.HasPrefix(session.SocketPath, tmpDir) {
		t.Errorf("Socket path should be in %s, got %s", tmpDir, session.SocketPath)
	}
	if !strings.HasSuffix(session.SocketPath, ".sock") {
		t.Errorf("Socket path should end with .sock, got %s", session.SocketPath)
	}

	if session.Context.UserID != "user1" {
		t.Errorf("Expected UserID user1, got %s", session.Context.UserID)
	}

	if len(session.MCPTools) != 1 {
		t.Errorf("Expected 1 MCP tool, got %d", len(session.MCPTools))
	}

	// Verify socket file exists
	if _, err := os.Stat(session.SocketPath); os.IsNotExist(err) {
		t.Error("Socket file should exist")
	}
}

// TestGetSession tests retrieving a session
func TestGetSession(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-get-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	sessionID := "test-get-session"
	agentCtx := &AgentContext{UserID: "user1", ChatID: "chat1"}

	// Get non-existent session
	_, ok := m.Get(sessionID)
	if ok {
		t.Error("Get should return false for non-existent session")
	}

	// Create session
	_, err = m.Create(ctx, sessionID, agentCtx, nil)
	if err != nil {
		t.Fatalf("Create session failed: %v", err)
	}
	defer m.Close(sessionID)

	// Get existing session
	session, ok := m.Get(sessionID)
	if !ok {
		t.Error("Get should return true for existing session")
	}

	if session.ID != sessionID {
		t.Errorf("Expected session ID %s, got %s", sessionID, session.ID)
	}
}

// TestCloseSession tests closing a session
func TestCloseSession(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-close-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	sessionID := "test-close-session"
	agentCtx := &AgentContext{UserID: "user1", ChatID: "chat1"}

	session, err := m.Create(ctx, sessionID, agentCtx, nil)
	if err != nil {
		t.Fatalf("Create session failed: %v", err)
	}

	socketPath := session.SocketPath

	// Close session
	err = m.Close(sessionID)
	if err != nil {
		t.Fatalf("Close session failed: %v", err)
	}

	// Verify session is removed
	_, ok := m.Get(sessionID)
	if ok {
		t.Error("Session should be removed after close")
	}

	// Verify socket file is removed (give it a moment)
	time.Sleep(100 * time.Millisecond)
	if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
		t.Error("Socket file should be removed after close")
	}
}

// TestCloseNonExistentSession tests closing a non-existent session
func TestCloseNonExistentSession(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-close-nonexist-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)

	// Should not error
	err = m.Close("nonexistent-session")
	if err != nil {
		t.Errorf("Close non-existent session should not error: %v", err)
	}
}

// TestCloseAllSessions tests closing all sessions
func TestCloseAllSessions(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-closeall-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	// Create multiple sessions
	sessionIDs := []string{"session-1", "session-2", "session-3"}
	for _, id := range sessionIDs {
		_, err := m.Create(ctx, id, &AgentContext{UserID: "user", ChatID: id}, nil)
		if err != nil {
			t.Fatalf("Create session %s failed: %v", id, err)
		}
	}

	// Verify sessions exist
	for _, id := range sessionIDs {
		if _, ok := m.Get(id); !ok {
			t.Errorf("Session %s should exist", id)
		}
	}

	// Close all
	m.CloseAll()

	// Verify all sessions are removed
	time.Sleep(100 * time.Millisecond)
	for _, id := range sessionIDs {
		if _, ok := m.Get(id); ok {
			t.Errorf("Session %s should be removed after CloseAll", id)
		}
	}
}

// TestSessionReplace tests that creating a session with existing ID replaces it
func TestSessionReplace(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-replace-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	sessionID := "test-replace-session"

	// Create first session
	session1, err := m.Create(ctx, sessionID, &AgentContext{UserID: "user1", ChatID: "chat1"}, nil)
	if err != nil {
		t.Fatalf("Create first session failed: %v", err)
	}
	socketPath1 := session1.SocketPath

	// Create second session with same ID
	session2, err := m.Create(ctx, sessionID, &AgentContext{UserID: "user2", ChatID: "chat2"}, nil)
	if err != nil {
		t.Fatalf("Create second session failed: %v", err)
	}
	defer m.Close(sessionID)

	// Verify second session replaced first
	if session2.Context.UserID != "user2" {
		t.Errorf("Expected UserID user2, got %s", session2.Context.UserID)
	}

	// Get session should return second
	session, ok := m.Get(sessionID)
	if !ok {
		t.Error("Get should return session")
	}
	if session.Context.UserID != "user2" {
		t.Errorf("Expected UserID user2 from Get, got %s", session.Context.UserID)
	}

	// Same socket path should be reused
	if session2.SocketPath != socketPath1 {
		t.Errorf("Expected same socket path, got %s vs %s", socketPath1, session2.SocketPath)
	}
}

// TestConcurrentSessionAccess tests concurrent access to sessions
func TestConcurrentSessionAccess(t *testing.T) {
	// Use /tmp for shorter socket path (macOS has 104 char limit for Unix sockets)
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	var wg sync.WaitGroup
	var mu sync.Mutex
	errors := make([]error, 0)
	numGoroutines := 5 // Reduced for stability

	// Concurrent creates
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			sessionID := fmt.Sprintf("s%d", idx) // Short session ID
			_, err := m.Create(ctx, sessionID, &AgentContext{UserID: "user", ChatID: sessionID}, nil)
			if err != nil {
				mu.Lock()
				errors = append(errors, fmt.Errorf("session %s: %v", sessionID, err))
				mu.Unlock()
			}
		}(i)
	}

	wg.Wait()

	// Check errors
	for _, err := range errors {
		t.Errorf("Concurrent create error: %v", err)
	}

	// Verify all sessions exist
	for i := 0; i < numGoroutines; i++ {
		sessionID := fmt.Sprintf("s%d", i)
		if _, ok := m.Get(sessionID); !ok {
			t.Errorf("Session %s should exist", sessionID)
		}
	}

	// Cleanup
	m.CloseAll()
}

// TestSessionConnection tests connecting to a session socket
func TestSessionConnection(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-connect-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	sessionID := "test-connect-session"
	agentCtx := &AgentContext{UserID: "user1", ChatID: "chat1"}
	mcpTools := map[string]*MCPTool{}

	session, err := m.Create(ctx, sessionID, agentCtx, mcpTools)
	if err != nil {
		t.Fatalf("Create session failed: %v", err)
	}
	defer m.Close(sessionID)

	// Give the listener time to start
	time.Sleep(50 * time.Millisecond)

	// Try to connect to the socket
	conn, err := net.Dial("unix", session.SocketPath)
	if err != nil {
		t.Fatalf("Failed to connect to socket: %v", err)
	}
	defer conn.Close()

	// Send initialize request
	initReq := JSONRPCRequest{
		JSONRPC: "2.0",
		ID:      1,
		Method:  "initialize",
		Params:  json.RawMessage(`{"protocolVersion":"2024-11-05"}`),
	}
	data, _ := json.Marshal(initReq)

	// Write with newline (NDJSON)
	_, err = conn.Write(append(data, '\n'))
	if err != nil {
		t.Fatalf("Failed to write to socket: %v", err)
	}

	// Set read deadline
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))

	// Read response
	buf := make([]byte, 4096)
	n, err := conn.Read(buf)
	if err != nil {
		t.Fatalf("Failed to read from socket: %v", err)
	}

	// Parse response
	var resp JSONRPCResponse
	if err := json.Unmarshal(buf[:n], &resp); err != nil {
		t.Fatalf("Failed to parse response: %v (raw: %s)", err, string(buf[:n]))
	}

	if resp.JSONRPC != "2.0" {
		t.Errorf("Expected JSONRPC 2.0, got %s", resp.JSONRPC)
	}

	if resp.Error != nil {
		t.Errorf("Unexpected error: %v", resp.Error)
	}

	if resp.Result == nil {
		t.Error("Expected result, got nil")
	}
}

// TestToolsList tests the tools/list method
func TestToolsList(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-tools-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	sessionID := "test-tools-session"
	agentCtx := &AgentContext{UserID: "user1", ChatID: "chat1"}
	mcpTools := map[string]*MCPTool{
		"tool1": {
			Name:        "tool1",
			Description: "First test tool",
			Process:     "scripts.test.tool1",
			InputSchema: json.RawMessage(`{"type":"object"}`),
		},
		"tool2": {
			Name:        "tool2",
			Description: "Second test tool",
			Process:     "scripts.test.tool2",
			InputSchema: json.RawMessage(`{"type":"object","properties":{"arg":{"type":"string"}}}`),
		},
	}

	session, err := m.Create(ctx, sessionID, agentCtx, mcpTools)
	if err != nil {
		t.Fatalf("Create session failed: %v", err)
	}
	defer m.Close(sessionID)

	time.Sleep(50 * time.Millisecond)

	conn, err := net.Dial("unix", session.SocketPath)
	if err != nil {
		t.Fatalf("Failed to connect to socket: %v", err)
	}
	defer conn.Close()

	// Send tools/list request
	req := JSONRPCRequest{
		JSONRPC: "2.0",
		ID:      2,
		Method:  "tools/list",
	}
	data, _ := json.Marshal(req)
	conn.Write(append(data, '\n'))

	// Read response
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	buf := make([]byte, 4096)
	n, err := conn.Read(buf)
	if err != nil {
		t.Fatalf("Failed to read from socket: %v", err)
	}

	var resp JSONRPCResponse
	if err := json.Unmarshal(buf[:n], &resp); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if resp.Error != nil {
		t.Fatalf("Unexpected error: %v", resp.Error)
	}

	// Parse result as ToolsListResult
	resultBytes, _ := json.Marshal(resp.Result)
	var toolsResult ToolsListResult
	if err := json.Unmarshal(resultBytes, &toolsResult); err != nil {
		t.Fatalf("Failed to parse tools result: %v", err)
	}

	if len(toolsResult.Tools) != 2 {
		t.Errorf("Expected 2 tools, got %d", len(toolsResult.Tools))
	}

	// Verify tool names
	toolNames := make(map[string]bool)
	for _, tool := range toolsResult.Tools {
		toolNames[tool.Name] = true
	}

	if !toolNames["tool1"] {
		t.Error("Expected tool1 in tools list")
	}
	if !toolNames["tool2"] {
		t.Error("Expected tool2 in tools list")
	}
}

// TestMethodNotFound tests handling of unknown methods
func TestMethodNotFound(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-notfound-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	session, err := m.Create(ctx, "test-notfound", &AgentContext{UserID: "user", ChatID: "chat"}, nil)
	if err != nil {
		t.Fatalf("Create session failed: %v", err)
	}
	defer m.Close("test-notfound")

	time.Sleep(50 * time.Millisecond)

	conn, err := net.Dial("unix", session.SocketPath)
	if err != nil {
		t.Fatalf("Failed to connect to socket: %v", err)
	}
	defer conn.Close()

	// Send unknown method
	req := JSONRPCRequest{
		JSONRPC: "2.0",
		ID:      3,
		Method:  "unknown/method",
	}
	data, _ := json.Marshal(req)
	conn.Write(append(data, '\n'))

	// Read response
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	buf := make([]byte, 4096)
	n, err := conn.Read(buf)
	if err != nil {
		t.Fatalf("Failed to read from socket: %v", err)
	}

	var resp JSONRPCResponse
	if err := json.Unmarshal(buf[:n], &resp); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if resp.Error == nil {
		t.Error("Expected error for unknown method")
	}

	if resp.Error != nil && resp.Error.Code != ErrCodeMethodNotFound {
		t.Errorf("Expected error code %d, got %d", ErrCodeMethodNotFound, resp.Error.Code)
	}
}

// TestParseError tests handling of invalid JSON
func TestParseError(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-parse-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	session, err := m.Create(ctx, "test-parse", &AgentContext{UserID: "user", ChatID: "chat"}, nil)
	if err != nil {
		t.Fatalf("Create session failed: %v", err)
	}
	defer m.Close("test-parse")

	time.Sleep(50 * time.Millisecond)

	conn, err := net.Dial("unix", session.SocketPath)
	if err != nil {
		t.Fatalf("Failed to connect to socket: %v", err)
	}
	defer conn.Close()

	// Send invalid JSON
	conn.Write([]byte("not valid json\n"))

	// Read response
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	buf := make([]byte, 4096)
	n, err := conn.Read(buf)
	if err != nil {
		t.Fatalf("Failed to read from socket: %v", err)
	}

	var resp JSONRPCResponse
	if err := json.Unmarshal(buf[:n], &resp); err != nil {
		t.Fatalf("Failed to parse response: %v", err)
	}

	if resp.Error == nil {
		t.Error("Expected error for invalid JSON")
	}

	if resp.Error != nil && resp.Error.Code != ErrCodeParse {
		t.Errorf("Expected error code %d, got %d", ErrCodeParse, resp.Error.Code)
	}
}

// TestInitializedNotification tests that initialized notification doesn't return response
func TestInitializedNotification(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "ipc-initialized-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	m := NewManager(tmpDir)
	ctx := context.Background()

	session, err := m.Create(ctx, "test-initialized", &AgentContext{UserID: "user", ChatID: "chat"}, nil)
	if err != nil {
		t.Fatalf("Create session failed: %v", err)
	}
	defer m.Close("test-initialized")

	time.Sleep(50 * time.Millisecond)

	conn, err := net.Dial("unix", session.SocketPath)
	if err != nil {
		t.Fatalf("Failed to connect to socket: %v", err)
	}
	defer conn.Close()

	// Send initialized notification (no ID = notification)
	req := JSONRPCRequest{
		JSONRPC: "2.0",
		Method:  "initialized",
	}
	data, _ := json.Marshal(req)
	conn.Write(append(data, '\n'))

	// Set short read deadline - we expect timeout since no response
	conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
	buf := make([]byte, 4096)
	_, err = conn.Read(buf)

	// Expect timeout (no response for notifications)
	if err == nil {
		t.Error("Expected no response for notification")
	}
}
