package agentruntime

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/tidbcloud/AgentTeam/internal/service/maas"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/rewind"
)

func TestPrepareAgentStartComposesStartCommandWithoutStoreFacade(t *testing.T) {
	ctx := context.Background()
	agents := &fakeAgentRuntimeStore{
		prepared: agent.PreparedAgentStart{
			AgentID:         "agt_1",
			WorkspaceID:     "wsp_1",
			MemberID:        "mem_1",
			Name:            "agent-one",
			DisplayName:     "Agent One",
			Description:     "Runtime agent",
			Runtime:         "codex",
			PermissionMode:  agent.AgentPermissionModeWorkspaceWrite,
			Model:           "gpt-5-codex",
			ReasoningEffort: "high",
			AuthToken:       "token_1",
			SessionID:       "sess_1",
		},
		materialization: &agent.WorkspaceMaterialization{
			SeedFiles: []agent.WorkspaceSeedFile{{Path: "MEMORY.md", Content: "remember"}},
		},
	}
	service := New(Dependencies{Agents: agents, Messages: fakeUnreadStore{}, Recovery: fakeRecoveryStore{}})

	wake := &message.ReceiveMessage{MessageID: "msg_1"}
	command, err := service.PrepareAgentStart(ctx, "agt_1", "https://agentteam.test", wake, true, false, false)
	if err != nil {
		t.Fatalf("PrepareAgentStart returned error: %v", err)
	}
	if command.AgentID != "agt_1" {
		t.Fatalf("AgentID = %q, want agt_1", command.AgentID)
	}
	if command.Config.Name != "agent-one" {
		t.Fatalf("Config.Name = %q, want agent-one", command.Config.Name)
	}
	if command.Config.ServerURL != "https://agentteam.test" {
		t.Fatalf("Config.ServerURL = %q", command.Config.ServerURL)
	}
	if command.Config.PermissionMode != agent.AgentPermissionModeFullAccess {
		t.Fatalf("Config.PermissionMode = %q, want %q", command.Config.PermissionMode, agent.AgentPermissionModeFullAccess)
	}
	if command.Config.Materialization == nil || len(command.Config.Materialization.SeedFiles) != 1 || command.Config.Materialization.SeedFiles[0].Content != "remember" {
		t.Fatalf("materialization not attached: %#v", command.Config.Materialization)
	}
	if command.WakeMessage != wake {
		t.Fatalf("wake message pointer was not preserved")
	}
	if command.UnreadSummary != nil {
		t.Fatalf("UnreadSummary = %#v, want nil for empty unread rows", command.UnreadSummary)
	}
	if agents.prepareAgentID != "agt_1" {
		t.Fatalf("PrepareAgentStart delegated with %q", agents.prepareAgentID)
	}
}

func TestPrepareAgentStartIncludesParentTargetedUnreadSummary(t *testing.T) {
	ctx := context.Background()
	agents := &fakeAgentRuntimeStore{
		prepared: agent.PreparedAgentStart{
			AgentID:     "agt_1",
			WorkspaceID: "wsp_1",
			MemberID:    "mem_1",
			Name:        "agent-one",
			Runtime:     "codex",
			AuthToken:   "token_1",
		},
	}
	service := New(Dependencies{
		Agents: agents,
		Messages: fakeUnreadStore{
			threads: []message.AgentUnreadThread{{
				ThreadID:          "thr_reply",
				ChannelType:       "thread",
				ChannelName:       "thread-abcd1234",
				ParentChannelType: "channel",
				ParentChannelName: "all",
				LastReadSeq:       0,
				UnreadCount:       1,
			}},
		},
		Recovery: fakeRecoveryStore{},
	})

	command, err := service.PrepareAgentStart(ctx, "agt_1", "https://agentteam.test", nil, true, false, false)
	if err != nil {
		t.Fatalf("PrepareAgentStart returned error: %v", err)
	}
	if got := command.UnreadSummary["#all:abcd1234"]; got != 1 {
		t.Fatalf("UnreadSummary = %#v, want parent-thread sparse delivery", command.UnreadSummary)
	}
}

func TestPrepareAgentStartBuildsAgentTeamProxyLLMConfig(t *testing.T) {
	ctx := context.Background()
	agents := &fakeAgentRuntimeStore{
		prepared: agent.PreparedAgentStart{
			AgentID:         "agt_1",
			WorkspaceID:     "wsp_1",
			MemberID:        "mem_1",
			Name:            "agent-one",
			DisplayName:     "Agent One",
			Description:     "Runtime agent",
			Runtime:         "codex",
			Model:           "gpt-5-codex",
			ReasoningEffort: "high",
			LLMMode:         agent.AgentLLMModeAgentTeamProxy,
			APIKey:          "lpi_test",
			AuthToken:       "token_1",
		},
	}
	service := New(Dependencies{
		Agents:   agents,
		Messages: fakeUnreadStore{},
		Recovery: fakeRecoveryStore{},
	})

	command, err := service.PrepareAgentStart(ctx, "agt_1", "https://agentteam.test", nil, true, false, false)
	if err != nil {
		t.Fatalf("PrepareAgentStart returned error: %v", err)
	}
	if command.Config.BaseURL != "https://agentteam.test/api/v1/workspaces/wsp_1/llm/v1" {
		t.Fatalf("BaseURL = %q", command.Config.BaseURL)
	}
	if command.Config.APIKey != "lpi_test" {
		t.Fatalf("APIKey = %q", command.Config.APIKey)
	}
	if got := command.Config.Headers["X-AgentTeam-Route-Workspace-Id"]; got != "wsp_1" {
		t.Fatalf("route workspace header = %q, want wsp_1", got)
	}
	if command.Config.WireAPI != "responses" {
		t.Fatalf("WireAPI = %q, want responses", command.Config.WireAPI)
	}
}

func TestPrepareAgentStartBuildsAgentTeamProxyLLMConfigWhenDirectMaaSEnabled(t *testing.T) {
	ctx := context.Background()
	agents := &fakeAgentRuntimeStore{
		prepared: agent.PreparedAgentStart{
			AgentID:         "agt_1",
			WorkspaceID:     "wsp_1",
			MemberID:        "mem_1",
			Name:            "agent-one",
			DisplayName:     "Agent One",
			Description:     "Runtime agent",
			Runtime:         "codex",
			Model:           "gpt-5-codex",
			ReasoningEffort: "high",
			LLMMode:         agent.AgentLLMModeAgentTeamProxy,
			APIKey:          "lpi_test",
			AuthToken:       "token_1",
		},
	}
	service := New(Dependencies{
		Agents:   agents,
		Messages: fakeUnreadStore{},
		Recovery: fakeRecoveryStore{},
	})

	command, err := service.PrepareAgentStart(ctx, "agt_1", "https://agentteam.test", nil, true, true, true)
	if err != nil {
		t.Fatalf("PrepareAgentStart returned error: %v", err)
	}
	if command.Config.BaseURL != "https://agentteam.test/api/v1/workspaces/wsp_1/llm/v1" {
		t.Fatalf("BaseURL = %q", command.Config.BaseURL)
	}
	if command.Config.APIKey != "lpi_test" {
		t.Fatalf("APIKey = %q", command.Config.APIKey)
	}
	if got := command.Config.Headers["X-AgentTeam-Route-Workspace-Id"]; got != "wsp_1" {
		t.Fatalf("route workspace header = %q, want wsp_1", got)
	}
	if command.Config.WireAPI != "responses" {
		t.Fatalf("WireAPI = %q, want responses", command.Config.WireAPI)
	}
}

func TestPrepareAgentStartRejectsAgentTeamProxyWithoutRouteWorkspace(t *testing.T) {
	agents := &fakeAgentRuntimeStore{
		prepared: agent.PreparedAgentStart{
			AgentID:   "agt_1",
			MemberID:  "mem_1",
			Name:      "agent-one",
			Runtime:   "codex",
			LLMMode:   agent.AgentLLMModeAgentTeamProxy,
			APIKey:    "lpi_test",
			AuthToken: "token_1",
		},
	}
	service := New(Dependencies{
		Agents:   agents,
		Messages: fakeUnreadStore{},
		Recovery: fakeRecoveryStore{},
	})

	_, err := service.PrepareAgentStart(context.Background(), "agt_1", "https://agentteam.test", nil, true, false, false)
	if err == nil || !strings.Contains(err.Error(), "resolve agent agt_1 llm for start: agentteam proxy route workspace ID is required") {
		t.Fatalf("PrepareAgentStart error = %v, want missing route workspace error", err)
	}
}

func TestPrepareAgentStartBuildsDirectMaaSAgentTeamHostedLLMConfig(t *testing.T) {
	ctx := context.Background()
	hosted := &fakeHostedAgentValidator{}
	agents := &fakeAgentRuntimeStore{
		prepared: agent.PreparedAgentStart{
			AgentID:         "agt_1",
			OrganizationID:  "org_1",
			WorkspaceID:     "wsp_1",
			MemberID:        "mem_1",
			Name:            "agent-one",
			DisplayName:     "Agent One",
			Description:     "Runtime agent",
			Runtime:         "claude",
			Model:           "agentteam/deepseek-v4-pro",
			ReasoningEffort: "high",
			LLMMode:         agent.AgentLLMModeAgentTeamHosted,
			AuthToken:       "token_1",
		},
	}
	service := New(Dependencies{
		Agents:             agents,
		Messages:           fakeUnreadStore{},
		Recovery:           fakeRecoveryStore{},
		HostedAgents:       hosted,
		MaaSRuntimeBaseURL: "http://127.0.0.1:8080/maas/v1",
	})

	command, err := service.PrepareAgentStart(ctx, "agt_1", "https://agentteam.test", nil, true, true, true)
	if err != nil {
		t.Fatalf("PrepareAgentStart returned error: %v", err)
	}
	if command.Config.BaseURL != "http://127.0.0.1:8080/maas/v1" {
		t.Fatalf("BaseURL = %q", command.Config.BaseURL)
	}
	if command.Config.APIKey != "" {
		t.Fatalf("APIKey = %q", command.Config.APIKey)
	}
	if command.Config.WireAPI != "anthropic" {
		t.Fatalf("WireAPI = %q, want anthropic", command.Config.WireAPI)
	}
	if command.Config.CredentialKind != agent.DaemonLLMCredentialKindMaaSCapability {
		t.Fatalf("CredentialKind = %q", command.Config.CredentialKind)
	}
	if hosted.last.ModelReference != "agentteam/deepseek-v4-pro" || hosted.last.LLMMode != agent.AgentLLMModeAgentTeamHosted {
		t.Fatalf("hosted validation = %#v", hosted.last)
	}
}

func TestStartAgentRuntimeForUserUsesNarrowPortsAndRuntimePolicy(t *testing.T) {
	ctx := context.Background()
	errOffline := errors.New("runtime offline")
	errUpgrade := errors.New("upgrade required")

	t.Run("ignores configured offline runtime errors after enabling auto wake", func(t *testing.T) {
		webAgents := &fakeWebAgentStore{
			autoWakeAgent: agent.AgentView{AgentID: "agt_1", MachineID: "mac_1"},
		}
		runtime := &fakeRuntimeControl{startErr: errOffline}
		service := New(Dependencies{
			WebAgents:        webAgents,
			Runtime:          runtime,
			IsRuntimeOffline: func(err error) bool { return errors.Is(err, errOffline) },
		})

		agent, err := service.StartAgentRuntimeForUser(ctx, "usr_1", "wsp_1", "agt_1")
		if err != nil {
			t.Fatalf("StartAgentRuntimeForUser returned error: %v", err)
		}
		if !webAgents.lastAutoWakeEnabled || webAgents.lastAutoWakeAgentID != "agt_1" {
			t.Fatalf("auto wake call = (%q, %v), want (agt_1, true)", webAgents.lastAutoWakeAgentID, webAgents.lastAutoWakeEnabled)
		}
		if len(runtime.startCalls) != 1 || runtime.startCalls[0] != "mac_1/agt_1" {
			t.Fatalf("start calls = %#v", runtime.startCalls)
		}
		if agent.AgentID != "agt_1" {
			t.Fatalf("agent = %#v", agent)
		}
	})

	t.Run("propagates non-offline runtime errors", func(t *testing.T) {
		webAgents := &fakeWebAgentStore{
			autoWakeAgent: agent.AgentView{AgentID: "agt_2", MachineID: "mac_2"},
		}
		service := New(Dependencies{
			WebAgents:        webAgents,
			Runtime:          &fakeRuntimeControl{startErr: errUpgrade},
			IsRuntimeOffline: func(error) bool { return false },
		})

		_, err := service.StartAgentRuntimeForUser(ctx, "usr_1", "wsp_1", "agt_2")
		if !errors.Is(err, errUpgrade) {
			t.Fatalf("StartAgentRuntimeForUser error = %v, want %v", err, errUpgrade)
		}
	})
}

// TestStartStopAgentRuntimeForUserConvergesPerDimension asserts the runtime
// command runs only when the run-state dimension differs — no duplicate Start
// when already running, no duplicate Stop when already stopped. The owner-scoped
// auto-wake update always runs first; the runtime-truth read that gates the
// command happens after it but is independent of the auto-wake mutation (the
// fake's running flag lives on the runtime, not the auto-wake store), so the
// ordering never distorts run truth. Offline errors stay tolerated. This guards
// the convergence used by both the Agent exec handler and the Human Web path.
func TestStartStopAgentRuntimeForUserConvergesPerDimension(t *testing.T) {
	ctx := context.Background()
	tests := []struct {
		name      string
		running   bool
		start     bool
		wantSends int
	}{
		{"start not running sends once", false, true, 1},
		{"start running does not resend", true, true, 0},
		{"stop running sends once", true, false, 1},
		{"stop not running does not resend", false, false, 0},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			webAgents := &fakeWebAgentStore{autoWakeAgent: agent.AgentView{AgentID: "agt_1", MachineID: "mac_1"}}
			runtime := &fakeRuntimeControl{running: tt.running}
			service := New(Dependencies{WebAgents: webAgents, Runtime: runtime, IsRuntimeOffline: func(error) bool { return false }})

			if tt.start {
				if _, err := service.StartAgentRuntimeForUser(ctx, "usr_1", "wsp_1", "agt_1"); err != nil {
					t.Fatalf("StartAgentRuntimeForUser error: %v", err)
				}
				if !webAgents.lastAutoWakeEnabled {
					t.Fatalf("auto wake not enabled")
				}
				if len(runtime.startCalls) != tt.wantSends {
					t.Fatalf("start calls = %#v, want %d", runtime.startCalls, tt.wantSends)
				}
				return
			}
			if _, err := service.StopAgentRuntimeForUser(ctx, "usr_1", "wsp_1", "agt_1"); err != nil {
				t.Fatalf("StopAgentRuntimeForUser error: %v", err)
			}
			if webAgents.lastAutoWakeEnabled {
				t.Fatalf("auto wake not disabled")
			}
			if len(runtime.stopCalls) != tt.wantSends {
				t.Fatalf("stop calls = %#v, want %d", runtime.stopCalls, tt.wantSends)
			}
		})
	}
}

type fakeAgentRuntimeStore struct {
	prepared        agent.PreparedAgentStart
	materialization *agent.WorkspaceMaterialization
	prepareAgentID  string
}

func (store *fakeAgentRuntimeStore) AuthenticateMachine(context.Context, string) (agent.MachineIdentity, error) {
	return agent.MachineIdentity{}, nil
}
func (store *fakeAgentRuntimeStore) MarkMachineReady(context.Context, agent.MachineIdentity, agent.ReadyPayload) error {
	return nil
}
func (store *fakeAgentRuntimeStore) MarkMachineDisconnected(context.Context, string) error {
	return nil
}
func (store *fakeAgentRuntimeStore) ListAgentsForMachine(context.Context, string) ([]agent.MachineAgent, error) {
	return nil, nil
}
func (store *fakeAgentRuntimeStore) UpdateAgentStatusFromMachine(context.Context, string, string, string) (bool, error) {
	return true, nil
}
func (store *fakeAgentRuntimeStore) UpdateAgentActivityFromMachine(context.Context, string, string, string, string) (bool, error) {
	return true, nil
}
func (store *fakeAgentRuntimeStore) UpdateAgentSessionFromMachine(context.Context, string, string, string, bool) (bool, error) {
	return true, nil
}
func (store *fakeAgentRuntimeStore) AgentBelongsToMachine(context.Context, string, string) (bool, error) {
	return true, nil
}
func (store *fakeAgentRuntimeStore) AgentNamesForMachine(context.Context, string, string) (string, string, bool, error) {
	return "", "", true, nil
}
func (store *fakeAgentRuntimeStore) AgentPermissionModeForMachine(context.Context, string, string) (string, bool, error) {
	return agent.AgentPermissionModeFullAccess, true, nil
}
func (store *fakeAgentRuntimeStore) MachineIDForAgent(_ context.Context, _ string, _ string) (string, error) {
	return "", nil
}
func (store *fakeAgentRuntimeStore) ClearWorkspaceWriteAgentAuthTokensForMachine(context.Context, string) error {
	return nil
}
func (store *fakeAgentRuntimeStore) IssueAgentAuthTokenForMachine(context.Context, string, string, bool) (string, error) {
	return "agent-token", nil
}
func (store *fakeAgentRuntimeStore) AgentRuntimeForStart(context.Context, string) (string, error) {
	return "codex", nil
}
func (store *fakeAgentRuntimeStore) PrepareAgentStart(_ context.Context, agentID string, _ bool, includeLegacySession bool) (agent.PreparedAgentStart, error) {
	store.prepareAgentID = agentID
	prepared := store.prepared
	if !includeLegacySession {
		prepared.SessionID = ""
	}
	return prepared, nil
}
func (store *fakeAgentRuntimeStore) LoadAgentWorkspaceMaterialization(context.Context, string) (*agent.WorkspaceMaterialization, error) {
	return store.materialization, nil
}

type fakeRecoveryStore struct{}

func (fakeRecoveryStore) RecordMessageRecoveryPointAndPruneOtherThreads(context.Context, string, string, string, string, string, string) ([]rewind.MessageRecoveryPoint, error) {
	return nil, nil
}

type fakeHostedAgentValidator struct {
	last maas.AgentBindingValidation
	err  error
}

func (fake *fakeHostedAgentValidator) ValidateAgentBinding(_ context.Context, params maas.AgentBindingValidation) error {
	fake.last = params
	return fake.err
}

type fakeWebAgentStore struct {
	autoWakeAgent        agent.AgentView
	workspaceAgent       agent.AgentView
	lastAutoWakeAgentID  string
	lastAutoWakeEnabled  bool
	lastWorkspaceAgentID string
	lastClearedSessionID string
}

func (store *fakeWebAgentStore) SetAgentAutoWakeForUser(_ context.Context, _ string, _ string, agentID string, enabled bool) (agent.AgentView, error) {
	store.lastAutoWakeAgentID = agentID
	store.lastAutoWakeEnabled = enabled
	store.autoWakeAgent.AutoWakeEnabled = enabled
	return store.autoWakeAgent, nil
}

func (store *fakeWebAgentStore) GetAgentWorkspaceReaderForUser(_ context.Context, _ string, _ string, agentID string) (agent.AgentView, error) {
	store.lastWorkspaceAgentID = agentID
	return store.workspaceAgent, nil
}

func (store *fakeWebAgentStore) ClearAgentSessionForWorkspace(_ context.Context, _ string, _ string, agentID string) error {
	store.lastClearedSessionID = agentID
	return nil
}

type fakeRuntimeControl struct {
	running    bool
	startErr   error
	startCalls []string
	stopCalls  []string
}

func (runtime *fakeRuntimeControl) StartAgent(_ context.Context, machineID string, agentID string) error {
	runtime.startCalls = append(runtime.startCalls, machineID+"/"+agentID)
	return runtime.startErr
}

func (runtime *fakeRuntimeControl) StopAgent(machineID string, agentID string) error {
	runtime.stopCalls = append(runtime.stopCalls, machineID+"/"+agentID)
	return nil
}

func (runtime *fakeRuntimeControl) AgentRuntimeRunning(string, string) bool { return runtime.running }

func (runtime *fakeRuntimeControl) ResetAgentSession(string, string) error { return nil }

func (runtime *fakeRuntimeControl) ResetAgentWorkspace(string, string) error { return nil }

func (runtime *fakeRuntimeControl) GCRecoveryPoints(string, string, []rewind.MessageRecoveryPoint) error {
	return nil
}

type fakeUnreadStore struct {
	threads []message.AgentUnreadThread
}

func (store fakeUnreadStore) ListAgentUnreadMessageThreads(context.Context, string, string) ([]message.AgentUnreadThread, error) {
	return store.threads, nil
}
