package main

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/YspCoder/clawgo/pkg/agent"
	"github.com/YspCoder/clawgo/pkg/config"
	"github.com/YspCoder/clawgo/pkg/nodes"
	"github.com/YspCoder/clawgo/pkg/providers"
)

type stubNodeProvider struct {
	content string
}

func (p stubNodeProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
	return &providers.LLMResponse{Content: p.content, FinishReason: "stop"}, nil
}

func (p stubNodeProvider) GetDefaultModel() string {
	return "stub-model"
}

func TestParseNodeRegisterArgsDefaults(t *testing.T) {
	t.Parallel()

	cfg := config.DefaultConfig()
	cfg.Gateway.Host = "gateway.example"
	cfg.Gateway.Port = 7788
	cfg.Gateway.Token = "cfg-token"

	opts, err := parseNodeRegisterArgs([]string{"--id", "edge-dev"}, cfg)
	if err != nil {
		t.Fatalf("parseNodeRegisterArgs failed: %v", err)
	}
	if opts.GatewayBase != "http://gateway.example:7788" {
		t.Fatalf("unexpected gateway base: %s", opts.GatewayBase)
	}
	if opts.Token != "cfg-token" {
		t.Fatalf("unexpected token: %s", opts.Token)
	}
	if opts.ID != "edge-dev" {
		t.Fatalf("unexpected id: %s", opts.ID)
	}
	if !opts.Capabilities.Run || !opts.Capabilities.Invoke || !opts.Capabilities.Model {
		t.Fatalf("expected default run/invoke/model capabilities, got %+v", opts.Capabilities)
	}
}

func TestParseNodeRegisterArgsTags(t *testing.T) {
	t.Parallel()

	cfg := config.DefaultConfig()
	opts, err := parseNodeRegisterArgs([]string{"--id", "edge-dev", "--tags", "vision,gpu"}, cfg)
	if err != nil {
		t.Fatalf("parseNodeRegisterArgs failed: %v", err)
	}
	if len(opts.Tags) != 2 || opts.Tags[0] != "vision" || opts.Tags[1] != "gpu" {
		t.Fatalf("unexpected tags: %+v", opts.Tags)
	}
}

func TestPostNodeRegisterSendsNodeInfo(t *testing.T) {
	t.Parallel()

	var gotAuth string
	var got nodes.NodeInfo
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/nodes/register" {
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
		gotAuth = r.Header.Get("Authorization")
		if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"ok":true}`))
	}))
	defer srv.Close()

	info := nodes.NodeInfo{
		ID:       "edge-dev",
		Name:     "Edge Dev",
		Endpoint: "http://edge.example:18790",
		Capabilities: nodes.Capabilities{
			Run: true, Invoke: true, Model: true,
		},
		Actions: []string{"run", "agent_task"},
		Models:  []string{"gpt-4o-mini"},
	}
	client := &http.Client{Timeout: 2 * time.Second}
	if err := postNodeRegister(context.Background(), client, srv.URL, "secret", info); err != nil {
		t.Fatalf("postNodeRegister failed: %v", err)
	}
	if gotAuth != "Bearer secret" {
		t.Fatalf("unexpected auth header: %s", gotAuth)
	}
	if got.ID != "edge-dev" || got.Endpoint != "http://edge.example:18790" {
		t.Fatalf("unexpected node payload: %+v", got)
	}
	if len(got.Actions) != 2 || got.Actions[1] != "agent_task" {
		t.Fatalf("unexpected actions: %+v", got.Actions)
	}
}

func TestPostNodeHeartbeatSendsNodeID(t *testing.T) {
	t.Parallel()

	var body map[string]string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/nodes/heartbeat" {
			t.Fatalf("unexpected path: %s", r.URL.Path)
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatalf("decode body: %v", err)
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer srv.Close()

	client := &http.Client{Timeout: 2 * time.Second}
	if err := postNodeHeartbeat(context.Background(), client, srv.URL, "", "edge-dev"); err != nil {
		t.Fatalf("postNodeHeartbeat failed: %v", err)
	}
	if strings.TrimSpace(body["id"]) != "edge-dev" {
		t.Fatalf("unexpected heartbeat body: %+v", body)
	}
}

func TestNodeAgentsFromConfigCollectsEnabledAgents(t *testing.T) {
	t.Parallel()

	cfg := config.DefaultConfig()
	cfg.Agents.Subagents["main"] = config.SubagentConfig{
		Enabled:     true,
		Type:        "router",
		DisplayName: "Main Agent",
		Role:        "orchestrator",
	}
	cfg.Agents.Subagents["coder"] = config.SubagentConfig{
		Enabled:     true,
		Type:        "worker",
		DisplayName: "Code Agent",
		Role:        "code",
	}
	cfg.Agents.Subagents["tester"] = config.SubagentConfig{
		Enabled:     false,
		Type:        "worker",
		DisplayName: "Test Agent",
		Role:        "test",
	}
	items := nodeAgentsFromConfig(cfg)
	if len(items) != 2 {
		t.Fatalf("expected 2 enabled agents, got %+v", items)
	}
	if items[0].ID != "coder" || items[1].ID != "main" {
		t.Fatalf("unexpected agent export order: %+v", items)
	}
}

func TestNodeWebsocketURL(t *testing.T) {
	t.Parallel()

	if got := nodeWebsocketURL("http://gateway.example:18790"); got != "ws://gateway.example:18790/nodes/connect" {
		t.Fatalf("unexpected ws url: %s", got)
	}
	if got := nodeWebsocketURL("https://gateway.example"); got != "wss://gateway.example/nodes/connect" {
		t.Fatalf("unexpected wss url: %s", got)
	}
}

func TestNodeSocketPingInterval(t *testing.T) {
	t.Parallel()

	if got := nodeSocketPingInterval(120); got != 25*time.Second {
		t.Fatalf("expected 25s cap, got %s", got)
	}
	if got := nodeSocketPingInterval(20); got != 10*time.Second {
		t.Fatalf("expected 10s floor, got %s", got)
	}
	if got := nodeSocketPingInterval(30); got != 15*time.Second {
		t.Fatalf("expected half heartbeat, got %s", got)
	}
}

func TestExecuteNodeRequestRunsLocalMainAgentTask(t *testing.T) {
	prevCfg := globalConfigPathOverride
	prevProviderFactory := nodeProviderFactory
	prevLoopFactory := nodeAgentLoopFactory
	prevExecutors := nodeLocalExecutors
	globalConfigPathOverride = filepath.Join(t.TempDir(), "config.json")
	nodeLocalExecutors = map[string]*nodeLocalExecutor{}
	nodeProviderFactory = func(cfg *config.Config) (providers.LLMProvider, error) {
		return stubNodeProvider{content: "main-local-ok"}, nil
	}
	nodeAgentLoopFactory = agent.NewAgentLoop
	defer func() {
		globalConfigPathOverride = prevCfg
		nodeProviderFactory = prevProviderFactory
		nodeAgentLoopFactory = prevLoopFactory
		nodeLocalExecutors = prevExecutors
	}()

	cfg := config.DefaultConfig()
	cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
	cfg.Agents.Subagents["main"] = config.SubagentConfig{
		Enabled: true,
		Type:    "router",
		Role:    "orchestrator",
	}
	if err := config.SaveConfig(globalConfigPathOverride, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}

	info := nodes.NodeInfo{ID: "edge-a", Name: "Edge A"}
	resp := executeNodeRequest(context.Background(), &http.Client{Timeout: time.Second}, info, nodeRegisterOptions{}, &nodes.Request{
		Action: "agent_task",
		Task:   "say ok",
	})
	if !resp.OK {
		t.Fatalf("expected ok response, got %+v", resp)
	}
	if got := strings.TrimSpace(resp.Payload["result"].(string)); got != "main-local-ok" {
		t.Fatalf("unexpected result: %+v", resp.Payload)
	}
	if got := strings.TrimSpace(resp.Payload["agent_id"].(string)); got != "main" {
		t.Fatalf("unexpected agent id: %+v", resp.Payload)
	}
}

func TestExecuteNodeRequestRunsLocalSubagentTask(t *testing.T) {
	prevCfg := globalConfigPathOverride
	prevProviderFactory := nodeProviderFactory
	prevLoopFactory := nodeAgentLoopFactory
	prevExecutors := nodeLocalExecutors
	globalConfigPathOverride = filepath.Join(t.TempDir(), "config.json")
	nodeLocalExecutors = map[string]*nodeLocalExecutor{}
	nodeProviderFactory = func(cfg *config.Config) (providers.LLMProvider, error) {
		return stubNodeProvider{content: "coder-local-ok"}, nil
	}
	nodeAgentLoopFactory = agent.NewAgentLoop
	defer func() {
		globalConfigPathOverride = prevCfg
		nodeProviderFactory = prevProviderFactory
		nodeAgentLoopFactory = prevLoopFactory
		nodeLocalExecutors = prevExecutors
	}()

	cfg := config.DefaultConfig()
	cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
	cfg.Agents.Subagents["main"] = config.SubagentConfig{
		Enabled: true,
		Type:    "router",
		Role:    "orchestrator",
	}
	cfg.Agents.Subagents["coder"] = config.SubagentConfig{
		Enabled: true,
		Type:    "worker",
		Role:    "code",
	}
	if err := os.MkdirAll(filepath.Join(cfg.Agents.Defaults.Workspace, "out"), 0755); err != nil {
		t.Fatalf("mkdir artifact dir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(cfg.Agents.Defaults.Workspace, "out", "result.txt"), []byte("artifact-body"), 0644); err != nil {
		t.Fatalf("write artifact: %v", err)
	}
	if err := config.SaveConfig(globalConfigPathOverride, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}

	info := nodes.NodeInfo{ID: "edge-b", Name: "Edge B"}
	resp := executeNodeRequest(context.Background(), &http.Client{Timeout: time.Second}, info, nodeRegisterOptions{}, &nodes.Request{
		Action: "agent_task",
		Task:   "write tests",
		Args:   map[string]interface{}{"remote_agent_id": "coder", "artifact_paths": []interface{}{"out/result.txt"}},
	})
	if !resp.OK {
		t.Fatalf("expected ok response, got %+v", resp)
	}
	if got := strings.TrimSpace(resp.Payload["result"].(string)); !strings.Contains(got, "coder-local-ok") {
		t.Fatalf("unexpected result: %+v", resp.Payload)
	}
	if got := strings.TrimSpace(resp.Payload["agent_id"].(string)); got != "coder" {
		t.Fatalf("unexpected agent id: %+v", resp.Payload)
	}
	artifacts, ok := resp.Payload["artifacts"].([]map[string]interface{})
	if !ok || len(artifacts) != 1 {
		t.Fatalf("expected one artifact, got %+v", resp.Payload["artifacts"])
	}
	if artifacts[0]["content_text"] != "artifact-body" {
		t.Fatalf("unexpected artifact payload: %+v", artifacts[0])
	}
}

func TestCollectNodeArtifactsRejectsPathEscape(t *testing.T) {
	t.Parallel()

	_, err := collectNodeArtifacts(t.TempDir(), map[string]interface{}{
		"artifact_paths": []interface{}{"../secret.txt"},
	})
	if err == nil || !strings.Contains(err.Error(), "escapes workspace") {
		t.Fatalf("expected workspace escape error, got %v", err)
	}
}

func TestExecuteNodeRequestRunsLocalCameraSnap(t *testing.T) {
	prevCfg := globalConfigPathOverride
	prevExecutors := nodeLocalExecutors
	prevCamera := nodeCameraSnapFunc
	globalConfigPathOverride = filepath.Join(t.TempDir(), "config.json")
	nodeLocalExecutors = map[string]*nodeLocalExecutor{}
	defer func() {
		globalConfigPathOverride = prevCfg
		nodeLocalExecutors = prevExecutors
		nodeCameraSnapFunc = prevCamera
	}()

	cfg := config.DefaultConfig()
	cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
	if err := config.SaveConfig(globalConfigPathOverride, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}
	nodeCameraSnapFunc = func(ctx context.Context, workspace string, args map[string]interface{}) (string, error) {
		out := filepath.Join(workspace, "artifacts", "node", "camera-test.jpg")
		if err := os.MkdirAll(filepath.Dir(out), 0755); err != nil {
			return "", err
		}
		if err := os.WriteFile(out, []byte("camera-bytes"), 0644); err != nil {
			return "", err
		}
		return out, nil
	}

	info := nodes.NodeInfo{ID: "edge-cam", Name: "Edge Cam"}
	resp := executeNodeRequest(context.Background(), &http.Client{Timeout: time.Second}, info, nodeRegisterOptions{}, &nodes.Request{
		Action: "camera_snap",
		Args:   map[string]interface{}{"facing": "front"},
	})
	if !resp.OK {
		t.Fatalf("expected ok response, got %+v", resp)
	}
	artifacts, ok := resp.Payload["artifacts"].([]map[string]interface{})
	if !ok || len(artifacts) != 1 {
		t.Fatalf("expected one artifact, got %+v", resp.Payload["artifacts"])
	}
	if artifacts[0]["name"] != "camera-test.jpg" {
		t.Fatalf("unexpected artifact: %+v", artifacts[0])
	}
}

func TestExecuteNodeRequestRunsLocalScreenSnapshot(t *testing.T) {
	prevCfg := globalConfigPathOverride
	prevExecutors := nodeLocalExecutors
	prevScreen := nodeScreenSnapFunc
	globalConfigPathOverride = filepath.Join(t.TempDir(), "config.json")
	nodeLocalExecutors = map[string]*nodeLocalExecutor{}
	defer func() {
		globalConfigPathOverride = prevCfg
		nodeLocalExecutors = prevExecutors
		nodeScreenSnapFunc = prevScreen
	}()

	cfg := config.DefaultConfig()
	cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
	if err := config.SaveConfig(globalConfigPathOverride, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}
	nodeScreenSnapFunc = func(ctx context.Context, workspace string, args map[string]interface{}) (string, error) {
		out := filepath.Join(workspace, "artifacts", "node", "screen-test.png")
		if err := os.MkdirAll(filepath.Dir(out), 0755); err != nil {
			return "", err
		}
		if err := os.WriteFile(out, []byte{0x89, 0x50, 0x4e, 0x47}, 0644); err != nil {
			return "", err
		}
		return out, nil
	}

	info := nodes.NodeInfo{ID: "edge-screen", Name: "Edge Screen"}
	resp := executeNodeRequest(context.Background(), &http.Client{Timeout: time.Second}, info, nodeRegisterOptions{}, &nodes.Request{
		Action: "screen_snapshot",
	})
	if !resp.OK {
		t.Fatalf("expected ok response, got %+v", resp)
	}
	artifacts, ok := resp.Payload["artifacts"].([]map[string]interface{})
	if !ok || len(artifacts) != 1 {
		t.Fatalf("expected one artifact, got %+v", resp.Payload["artifacts"])
	}
	if artifacts[0]["name"] != "screen-test.png" {
		t.Fatalf("unexpected artifact: %+v", artifacts[0])
	}
}

func TestExecuteNodeRequestRunsLocalCameraClip(t *testing.T) {
	prevCfg := globalConfigPathOverride
	prevExecutors := nodeLocalExecutors
	prevClip := nodeCameraClipFunc
	globalConfigPathOverride = filepath.Join(t.TempDir(), "config.json")
	nodeLocalExecutors = map[string]*nodeLocalExecutor{}
	defer func() {
		globalConfigPathOverride = prevCfg
		nodeLocalExecutors = prevExecutors
		nodeCameraClipFunc = prevClip
	}()

	cfg := config.DefaultConfig()
	cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
	if err := config.SaveConfig(globalConfigPathOverride, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}
	nodeCameraClipFunc = func(ctx context.Context, workspace string, args map[string]interface{}) (string, error) {
		out := filepath.Join(workspace, "artifacts", "node", "camera-test.mp4")
		if err := os.MkdirAll(filepath.Dir(out), 0755); err != nil {
			return "", err
		}
		if err := os.WriteFile(out, []byte("video-bytes"), 0644); err != nil {
			return "", err
		}
		return out, nil
	}

	info := nodes.NodeInfo{ID: "edge-clip", Name: "Edge Clip"}
	resp := executeNodeRequest(context.Background(), &http.Client{Timeout: time.Second}, info, nodeRegisterOptions{}, &nodes.Request{
		Action: "camera_clip",
		Args:   map[string]interface{}{"duration_ms": 2500},
	})
	if !resp.OK {
		t.Fatalf("expected ok response, got %+v", resp)
	}
	if got, _ := resp.Payload["duration_ms"].(int); got != 2500 {
		t.Fatalf("unexpected duration payload: %+v", resp.Payload)
	}
	artifacts, ok := resp.Payload["artifacts"].([]map[string]interface{})
	if !ok || len(artifacts) != 1 {
		t.Fatalf("expected one artifact, got %+v", resp.Payload["artifacts"])
	}
	if artifacts[0]["name"] != "camera-test.mp4" {
		t.Fatalf("unexpected artifact: %+v", artifacts[0])
	}
}

func TestExecuteNodeRequestRunsLocalScreenRecord(t *testing.T) {
	prevCfg := globalConfigPathOverride
	prevExecutors := nodeLocalExecutors
	prevRecord := nodeScreenRecordFunc
	globalConfigPathOverride = filepath.Join(t.TempDir(), "config.json")
	nodeLocalExecutors = map[string]*nodeLocalExecutor{}
	defer func() {
		globalConfigPathOverride = prevCfg
		nodeLocalExecutors = prevExecutors
		nodeScreenRecordFunc = prevRecord
	}()

	cfg := config.DefaultConfig()
	cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
	if err := config.SaveConfig(globalConfigPathOverride, cfg); err != nil {
		t.Fatalf("save config: %v", err)
	}
	nodeScreenRecordFunc = func(ctx context.Context, workspace string, args map[string]interface{}) (string, error) {
		out := filepath.Join(workspace, "artifacts", "node", "screen-test.mp4")
		if err := os.MkdirAll(filepath.Dir(out), 0755); err != nil {
			return "", err
		}
		if err := os.WriteFile(out, []byte("screen-video"), 0644); err != nil {
			return "", err
		}
		return out, nil
	}

	info := nodes.NodeInfo{ID: "edge-record", Name: "Edge Record"}
	resp := executeNodeRequest(context.Background(), &http.Client{Timeout: time.Second}, info, nodeRegisterOptions{}, &nodes.Request{
		Action: "screen_record",
		Args:   map[string]interface{}{"duration_ms": 1800},
	})
	if !resp.OK {
		t.Fatalf("expected ok response, got %+v", resp)
	}
	if got, _ := resp.Payload["duration_ms"].(int); got != 1800 {
		t.Fatalf("unexpected duration payload: %+v", resp.Payload)
	}
	artifacts, ok := resp.Payload["artifacts"].([]map[string]interface{})
	if !ok || len(artifacts) != 1 {
		t.Fatalf("expected one artifact, got %+v", resp.Payload["artifacts"])
	}
	if artifacts[0]["name"] != "screen-test.mp4" {
		t.Fatalf("unexpected artifact: %+v", artifacts[0])
	}
}
