package dispatcher

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/promptcompiler"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/manager"
)

func TestBindingFromSessionPreservesAgentTeamReplyTarget(t *testing.T) {
	sessionMessage := &types.Message{
		ID: "local-msg",
		Input: types.MessageInput{
			Channel: "chn_48CroYy2iZem",
			Prompt:  "@agentteam1-claude please fix this thread bug",
			Meta: &types.AgentTeamMeta{
				ID:           "00000014fdde160dc7325109eed9bf54",
				AuthorTarget: "vwjokl4",
				Target:       "#agentteam:00000014",
				Timestamp:    "2026-04-08T02:40:02+08:00",
			},
		},
	}

	_, inbox, err := bindingFromSession(sessionMessage)
	require.NoError(t, err)
	require.Equal(t, "#agentteam:00000014", inbox.Target)
	require.Equal(t, "chn_48CroYy2iZem", inbox.ChannelName)
}

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

	for _, test := range []struct {
		name      string
		target    string
		want      [4]string
		wantEmpty bool
	}{
		{name: "channel", target: "#ops", want: [4]string{"channel", "ops", "", ""}},
		{name: "channel reply", target: "#ops:AbCd1234", want: [4]string{"thread", "abcd1234", "channel", "ops"}},
		{name: "dm", target: "DM:@alpha-agent", want: [4]string{"dm", "alpha-agent", "", ""}},
		{name: "dm reply", target: "dm:@alpha-agent:AbCd1234", want: [4]string{"thread", "abcd1234", "dm", "alpha-agent"}},
		{name: "task", target: "#tsk_abc123", want: [4]string{"task", "tsk_abc123", "", ""}},
		{name: "workflow", target: "workflow:pln_abc123", want: [4]string{"workflow", "pln_abc123", "", ""}},
		{name: "team", target: "#team_abcdefghijklmn", want: [4]string{"channel", "team_abcdefghijklmn", "", ""}},
		{name: "team reply", target: "#team_abcdefghijklmn:AbCd1234", want: [4]string{"thread", "abcd1234", "channel", "team_abcdefghijklmn"}},
		{name: "malformed task", target: "#tsk_Bad", wantEmpty: true},
		{name: "malformed team", target: "#team_short", wantEmpty: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			channelType, channelName, parentType, parentName := agentteamTargetFields(test.target)
			got := [4]string{channelType, channelName, parentType, parentName}
			if test.wantEmpty {
				require.Equal(t, [4]string{}, got)
				return
			}
			require.Equal(t, test.want, got)
		})
	}
}

func TestBindingFromSessionPreservesAgentTeamChannelDisplayName(t *testing.T) {
	sessionMessage := &types.Message{
		ID: "local-msg",
		Input: types.MessageInput{
			Channel: "chn_display",
			Prompt:  "please check this",
			Meta: &types.AgentTeamMeta{
				ID:                       "msg_display",
				AuthorTarget:             "alice",
				Target:                   "#1:deadbeef",
				ChannelDisplayName:       "中文频道 1",
				ParentChannelDisplayName: "中文频道 1",
				Timestamp:                "2026-04-30T15:28:00Z",
			},
		},
	}

	_, inbox, err := bindingFromSession(sessionMessage)
	require.NoError(t, err)
	require.Equal(t, "中文频道 1", inbox.ChannelDisplayName)
	require.Equal(t, "中文频道 1", inbox.ParentChannelDisplayName)
}

func TestBindingFromSessionPreservesAgentTeamInterruptPolicy(t *testing.T) {
	sessionMessage := &types.Message{
		ID: "local-msg",
		Input: types.MessageInput{
			Channel: "chn_agentteam",
			Prompt:  "please switch now",
			Meta: &types.AgentTeamMeta{
				ID:              "msg_interrupt",
				AuthorTarget:    "alice",
				Target:          "#agentteam:abc123ef",
				Timestamp:       "2026-05-29T12:00:00Z",
				InterruptPolicy: "interrupt_current_run",
			},
		},
	}

	_, inbox, err := bindingFromSession(sessionMessage)
	require.NoError(t, err)
	require.Equal(t, "interrupt_current_run", inbox.InterruptPolicy)
}

func TestBindingFromSessionPreservesNativeServiceEventSource(t *testing.T) {
	sessionMessage := &types.Message{
		ID: "local-msg",
		Input: types.MessageInput{
			Channel: "chn_agentteam",
			Prompt:  "provider update",
			Meta: &types.AgentTeamMeta{
				ID:           "msg_provider",
				AuthorTarget: "gitea",
				Target:       "#agentteam",
				Timestamp:    "2026-05-28T12:00:00Z",
				Source: types.ContributionSource{
					Kind:      "provider_event",
					RefType:   "service_event_delivery",
					RefID:     "sed_1",
					RunID:     "sed_1",
					EventID:   "evt_1",
					EventType: "gitea.pull_request.synchronized",
					Label:     "Gitea PR watch",
				},
			},
		},
	}

	_, inbox, err := bindingFromSession(sessionMessage)
	require.NoError(t, err)
	require.Equal(t, "provider_event", inbox.Source.Kind)
	require.Equal(t, "service_event_delivery", inbox.Source.RefType)
	require.Equal(t, "sed_1", inbox.Source.RunID)
	require.Equal(t, "evt_1", inbox.Source.EventID)
	require.Equal(t, "Gitea PR watch", inbox.Source.Label)
}

func TestBindingFromSessionPreservesAgentTeamReactionMetadata(t *testing.T) {
	sessionMessage := &types.Message{
		ID: "local-msg",
		Input: types.MessageInput{
			Channel: "chn_agentteam",
			Prompt:  "reacted 👍 to your message",
			Meta: &types.AgentTeamMeta{
				ID:           "rxdlv_123",
				AuthorTarget: "alice",
				Target:       "#agentteam",
				Timestamp:    "2026-04-15T10:00:00Z",
				MessageType:  "reaction",
				Metadata: map[string]any{
					"reaction": map[string]any{
						"emoji":                "👍",
						"targetMessageId":      "msg_123",
						"targetMessagePreview": "Looks good to me",
					},
				},
			},
		},
	}

	_, inbox, err := bindingFromSession(sessionMessage)
	require.NoError(t, err)
	require.Equal(t, "reaction", inbox.MessageType)
	require.Equal(t, "msg_123", inbox.Metadata["reaction"].(map[string]any)["targetMessageId"])
}

func TestBindingFromSessionDerivesActiveTaskTarget(t *testing.T) {
	sessionMessage := &types.Message{
		ID: "task-msg",
		Input: types.MessageInput{
			Channel: "chn_agentteam",
			Prompt:  "Task discussion update for #t12",
			Meta: &types.AgentTeamMeta{
				ID:           "tds_1",
				AuthorTarget: "alice",
				Target:       "#agentteam:abc123ef",
				Timestamp:    "2026-04-15T10:00:00Z",
				MessageType:  "task_dispatch",
				Metadata: map[string]any{
					"task": map[string]any{
						"channelRef":       "#agentteam",
						"taskNumber":       12,
						"rootTaskNumber":   10,
						"parentTaskNumber": 11,
					},
					"taskDiscussion": map[string]any{
						"entryId": "dsc_1",
					},
				},
			},
		},
	}

	binding, inbox, err := bindingFromSession(sessionMessage)
	require.NoError(t, err)
	require.Equal(t, types.ActiveTaskTarget{
		Channel:          "#agentteam",
		TaskNumber:       12,
		Source:           "task_dispatch",
		RootTaskNumber:   10,
		ParentTaskNumber: 11,
	}, binding.activeTaskTarget)
	// The message thread target must not be backfilled into the active task's
	// ThreadTarget — task provenance/context is not a routing reply target.
	require.Empty(t, binding.activeTaskTarget.ThreadTarget)
	require.Equal(t, binding.activeTaskTarget, inbox.ActiveTaskTarget)
}

func TestBindingFromSessionUsesExplicitAgentTeamActiveTaskTarget(t *testing.T) {
	explicitTarget := types.ActiveTaskTarget{
		Channel:        "#agentteam",
		TaskNumber:     14,
		Source:         "human_selected",
		ThreadTarget:   "#agentteam:abc123ef",
		Role:           "worker",
		RootTaskNumber: 10,
	}
	sessionMessage := &types.Message{
		ID: "thread-msg",
		Input: types.MessageInput{
			Channel: "chn_agentteam",
			Prompt:  "please continue with the focused task",
			Meta: &types.AgentTeamMeta{
				ID:               "msg_1",
				AuthorTarget:     "alice",
				Target:           "#agentteam:abc123ef",
				Timestamp:        "2026-04-15T10:00:00Z",
				ActiveTaskTarget: explicitTarget,
			},
		},
	}

	binding, inbox, err := bindingFromSession(sessionMessage)
	require.NoError(t, err)
	require.Equal(t, explicitTarget, binding.activeTaskTarget)
	require.Equal(t, explicitTarget, inbox.ActiveTaskTarget)
}

func TestBindingFromSessionDerivesWorkflowContextAndStripsRawMetadata(t *testing.T) {
	sessionMessage := &types.Message{
		ID: "workflow-msg",
		Input: types.MessageInput{
			Channel: "chn_agentteam",
			Prompt:  "workflow authored message",
			Meta: &types.AgentTeamMeta{
				ID:           "msg_workflow",
				AuthorTarget: "workflow",
				Target:       "#agentteam",
				Timestamp:    "2026-07-08T08:50:00Z",
				Metadata: map[string]any{
					types.WorkflowContextMetadataWorkspaceIDKey:      "wsp_1",
					types.WorkflowContextMetadataPlanIDKey:           "wf_1",
					types.WorkflowContextMetadataPlanVersionIDKey:    "wfv_1",
					types.WorkflowContextMetadataRunIDKey:            "wfr_1",
					types.WorkflowContextMetadataEffectIDKey:         "wfs_1",
					types.WorkflowContextMetadataNodeIDKey:           "notify_agent",
					types.WorkflowContextMetadataSourceKindKey:       "schedule",
					types.WorkflowContextMetadataCallbackExpectedKey: true,
					"custom": "visible",
				},
			},
		},
	}

	_, inbox, err := bindingFromSession(sessionMessage)
	require.NoError(t, err)
	require.Equal(t, types.WorkflowContext{
		WorkspaceID:      "wsp_1",
		PlanID:           "wf_1",
		PlanVersionID:    "wfv_1",
		RunID:            "wfr_1",
		EffectID:         "wfs_1",
		NodeID:           "notify_agent",
		SourceKind:       "schedule",
		CallbackExpected: true,
	}, inbox.WorkflowContext)
	require.Equal(t, map[string]any{"custom": "visible"}, inbox.Metadata)
	require.NotContains(t, inbox.Metadata, types.WorkflowContextMetadataRunIDKey)
}

func TestPersistActiveTaskTargetRoundTrip(t *testing.T) {
	workDir := t.TempDir()
	target := types.ActiveTaskTarget{
		Channel:        "#agentteam",
		TaskNumber:     12,
		Source:         "task_tool_call",
		ThreadTarget:   "#agentteam:abc123ef",
		RootTaskNumber: 12,
	}

	persistActiveTaskTarget(workDir, target)

	require.Equal(t, target, loadPersistedActiveTaskTarget(workDir))
}

func TestShouldCarrySessionActiveTaskTargetSkipsHumanSelected(t *testing.T) {
	require.False(t, shouldCarrySessionActiveTaskTarget(types.ActiveTaskTarget{
		Channel:    "#agentteam",
		TaskNumber: 12,
		Source:     "human_selected",
	}))
	require.True(t, shouldCarrySessionActiveTaskTarget(types.ActiveTaskTarget{
		Channel:    "#agentteam",
		TaskNumber: 12,
		Source:     "task_tool_call",
	}))
}

func TestSetActiveTaskTargetUpdatesSnapshotAndPersistence(t *testing.T) {
	workDir := t.TempDir()
	d := mustNewDispatcher("")
	d.sessionManager.Set("agentteam1-codex", &interactiveSession{
		agentID: "agentteam1-codex",
		agent:   agent.Config{ID: "agentteam1-codex"},
		binding: conversationBinding{
			channelID:       "chn_agentteam",
			targetScopeType: "channel",
			conversationKey: "#agentteam",
		},
		workDir: workDir,
	})
	target := types.ActiveTaskTarget{
		Channel:    "#agentteam",
		TaskNumber: 12,
		Source:     "task_tool_call",
	}

	require.True(t, d.SetActiveTaskTarget("agentteam1-codex", target))
	snap, ok := d.Session("agentteam1-codex")
	require.True(t, ok)
	require.Equal(t, target.Normalize(), snap.ActiveTaskTarget)
	require.Equal(t, target.Normalize(), loadPersistedActiveTaskTarget(workDir))
}

type runningStubDriver struct{}

func (runningStubDriver) Name() string { return "stub" }

func (runningStubDriver) SupportsStdinNotification() bool { return false }

func (runningStubDriver) MCPToolPrefix() string { return "" }

func (runningStubDriver) Spawn(ctx context.Context, cfg drivercontract.LaunchConfig) (drivercontract.SpawnResult, error) {
	cmd := exec.CommandContext(ctx, "cat")
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return drivercontract.SpawnResult{}, err
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return drivercontract.SpawnResult{}, err
	}
	if err := cmd.Start(); err != nil {
		return drivercontract.SpawnResult{}, err
	}
	return drivercontract.SpawnResult{
		Cmd:    cmd,
		Stdin:  stdin,
		Stdout: stdout,
	}, nil
}

func (runningStubDriver) ParseLine(string) []drivercontract.ParsedEvent { return nil }

func (runningStubDriver) EncodeStdinMessage(string, string) []byte { return nil }

func TestDeliverSessionRefreshesAgentTeamBindingMetaForExistingSession(t *testing.T) {
	d := mustNewDispatcher("")

	proc, err := agent.NewProcess(
		context.Background(),
		runningStubDriver{},
		drivercontract.LaunchConfig{
			AgentID: "agentteam1-claude",
			WorkDir: t.TempDir(),
		},
		agent.ProcessOptions{},
	)
	require.NoError(t, err)
	t.Cleanup(func() {
		proc.Stop()
		require.Eventually(t, func() bool { return proc.State() == "stopped" }, time.Second, 10*time.Millisecond)
	})

	existingBinding := conversationBinding{
		channelID:       "chn_48CroYy2iZem",
		targetScopeType: "channel",
		conversationKey: "#agentteam:00000047",
		serverAgentID:   "agentteam1-claude",
	}
	existingKey := sessionKeyForBinding("agentteam1-claude", existingBinding).String()
	d.sessionManager.Set(existingKey, &interactiveSession{
		sessionKey:    existingKey,
		agentID:       "agentteam1-claude",
		agent:         agent.Config{ID: "agentteam1-claude"},
		binding:       existingBinding,
		process:       proc,
		serverAgentID: "agentteam1-claude",
	})

	_, err = d.DeliverSession(context.Background(), agent.Config{ID: "agentteam1-claude"}, runtimeconfig.Runtime{}, &types.Message{
		ID: "local-msg",
		Input: types.MessageInput{
			Channel: "chn_48CroYy2iZem",
			Prompt:  "@agentteam1-claude please reply in this thread",
			Meta: &types.AgentTeamMeta{
				ID:           "00000048fdde160dc7325109eed9bf54",
				AuthorTarget: "vwjokl4",
				Target:       "#agentteam:00000047",
				AgentID:      "agentteam1-claude",
				Timestamp:    "2026-04-08T09:29:19+08:00",
			},
		},
	})
	require.NoError(t, err)

	snap, ok := d.Session("agentteam1-claude")
	require.True(t, ok)

	agentteamMeta, ok := snap.Metadata.(*types.AgentTeamMeta)
	require.True(t, ok)
	require.Equal(t, "#agentteam:00000047", agentteamMeta.Target)
}

func TestDeliverSessionStartFailureEmitsWakeFailed(t *testing.T) {
	configDir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, configDir)

	agentID := "agentteam1-compile-fail"
	workDir := filepath.Join(configDir, "agents", agentID)
	require.NoError(t, os.MkdirAll(workDir, 0o755))
	oversizedDescription := string(bytes.Repeat([]byte("review "), promptcompiler.DefaultMaxBlockTokens+1))

	d := mustNewDispatcher("http://server.test")
	ch := &recordingChannel{id: "chn_agentteam"}
	mgr := manager.New(nil, nil)
	mgr.Register(ch)
	d.serverConnections = mgr

	_, err := d.DeliverSession(context.Background(), agent.Config{
		ID:          agentID,
		Name:        "reviewer",
		Description: oversizedDescription,
	}, runtimeconfig.Runtime{Type: runtimeconfig.TypeCodex}, &types.Message{
		ID: "local-msg",
		Input: types.MessageInput{
			Channel: "chn_agentteam",
			Prompt:  "@agentteam1-compile-fail please inspect this",
			Meta: &types.AgentTeamMeta{
				ID:           "msg-compile-fail",
				AuthorTarget: "alice",
				Target:       "#agentteam:00000047",
				AgentID:      agentID,
				Timestamp:    "2026-04-20T09:29:19Z",
			},
		},
	})
	require.Error(t, err)

	var runtimeEvents []types.RuntimeEvent
	for _, event := range ch.events {
		runtimeEvent, ok := event.(types.RuntimeEvent)
		if ok {
			runtimeEvents = append(runtimeEvents, runtimeEvent)
		}
	}
	require.Len(t, runtimeEvents, 1)
	require.Equal(t, consts.RuntimeEventClassWakeFailed, runtimeEvents[0].EventClass)
	require.Equal(t, agentID, runtimeEvents[0].AgentID)
	require.Equal(t, "msg-compile-fail", runtimeEvents[0].MessageID)
	require.Equal(t, "#agentteam:00000047", runtimeEvents[0].Target)
	require.Contains(t, runtimeEvents[0].Detail, `materialized block "agent_config.initial_role"`)
}
