package mcp

import (
	"context"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/workspace"
)

// ===== fakes =====

// fakeACPBridge implements ACPBridge.
type fakeACPBridge struct {
	kinds      []ACPAgentKind
	sessions   []ACPSession
	perms      []ACPPermission
	terminated []uuid.UUID
	turns      map[uuid.UUID][]ACPTurn
	dashboard  *ACPRunDashboard
	dashIn     ACPRunDashboardInput
}

func (f *fakeACPBridge) ListAgentKinds(_ context.Context, _ uuid.UUID, _ bool) ([]ACPAgentKind, error) {
	return f.kinds, nil
}
func (f *fakeACPBridge) GetAgentKind(_ context.Context, _ uuid.UUID, _ bool, id uuid.UUID) (*ACPAgentKind, error) {
	for i := range f.kinds {
		if f.kinds[i].ID == id {
			return &f.kinds[i], nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "agent kind")
}
func (f *fakeACPBridge) CreateSession(_ context.Context, userID uuid.UUID, _ bool, in ACPCreateSessionInput) (*ACPSession, error) {
	s := ACPSession{
		ID: uuid.New(), WorkspaceID: in.WorkspaceID, ProjectID: testPID,
		AgentKindID: in.AgentKindID, UserID: userID,
		Status: "starting", StartedAt: time.Now(),
	}
	if in.Branch != nil {
		s.Branch = *in.Branch
	}
	f.sessions = append(f.sessions, s)
	return &s, nil
}
func (f *fakeACPBridge) GetSession(_ context.Context, _ uuid.UUID, _ bool, id uuid.UUID) (*ACPSession, error) {
	for i := range f.sessions {
		if f.sessions[i].ID == id {
			return &f.sessions[i], nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "session")
}
func (f *fakeACPBridge) ListSessions(_ context.Context, _ uuid.UUID, _ bool, _ ACPSessionFilter) ([]ACPSession, error) {
	return f.sessions, nil
}
func (f *fakeACPBridge) TerminateSession(_ context.Context, _ uuid.UUID, _ bool, id uuid.UUID) error {
	f.terminated = append(f.terminated, id)
	return nil
}
func (f *fakeACPBridge) ListPendingPermissions(_ context.Context, _ uuid.UUID, _ bool, sessionID uuid.UUID) ([]ACPPermission, error) {
	out := make([]ACPPermission, 0, len(f.perms))
	for _, p := range f.perms {
		if p.SessionID == sessionID {
			out = append(out, p)
		}
	}
	return out, nil
}
func (f *fakeACPBridge) ListPendingApprovals(_ context.Context, _ uuid.UUID, _ bool) ([]ACPPermission, error) {
	out := make([]ACPPermission, 0, len(f.perms))
	out = append(out, f.perms...)
	return out, nil
}
func (f *fakeACPBridge) ListSessionTurns(_ context.Context, _ uuid.UUID, _ bool, sessionID uuid.UUID) ([]ACPTurn, error) {
	return f.turns[sessionID], nil
}
func (f *fakeACPBridge) GetRunDashboard(_ context.Context, _ uuid.UUID, _ bool, in ACPRunDashboardInput) (*ACPRunDashboard, error) {
	f.dashIn = in
	if f.dashboard != nil {
		return f.dashboard, nil
	}
	return &ACPRunDashboard{From: time.Now().Add(-24 * time.Hour), To: time.Now()}, nil
}

func acpTestDeps() (ServerDeps, *fakeACPBridge) {
	deps := testDeps()
	deps.Workspaces = &fakeWorkspaceSvc{items: []*workspace.Workspace{{
		ID: testWSID, ProjectID: testPID, Slug: "ws-1", Name: "Workspace 1",
		DefaultBranch: "main", SyncStatus: workspace.SyncStatusIdle, CreatedAt: time.Now(),
	}}}
	bridge := &fakeACPBridge{}
	deps.ACP = bridge
	return deps, bridge
}

// ===== tests =====

func TestListAndGetAgentKinds(t *testing.T) {
	deps, bridge := acpTestDeps()
	bridge.kinds = []ACPAgentKind{{
		ID: uuid.New(), Name: "claude_code", DisplayName: "Claude Code",
		Enabled: true, ToolAllowlist: []string{"*"},
	}}

	result, err := callTool(ctxWithAuth(Scope{}), deps, "list_agent_kinds", map[string]any{})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var listOut listAgentKindsOut
	unmarshalStructured(t, result, &listOut)
	require.Len(t, listOut.AgentKinds, 1)
	assert.Equal(t, "claude_code", listOut.AgentKinds[0].Name)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "get_agent_kind", map[string]any{
		"agent_kind_id": bridge.kinds[0].ID.String(),
	})
	require.NoError(t, err)
	var getOut agentKindDetail
	unmarshalStructured(t, result, &getOut)
	assert.Equal(t, "Claude Code", getOut.DisplayName)
}

func TestACPSessionLifecycle(t *testing.T) {
	deps, bridge := acpTestDeps()

	result, err := callTool(ctxWithAuth(Scope{}), deps, "create_acp_session", map[string]any{
		"workspace_id": testWSID.String(), "agent_kind_id": uuid.New().String(),
		"branch": "feat/x", "initial_prompt": "do something",
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var createOut acpSessionCreateOut
	unmarshalStructured(t, result, &createOut)
	assert.True(t, createOut.OK)
	assert.Equal(t, "feat/x", createOut.Session.Branch)
	sid := createOut.Session.ID

	result, err = callTool(ctxWithAuth(Scope{}), deps, "get_acp_session", map[string]any{
		"session_id": sid,
	})
	require.NoError(t, err)
	var getOut acpSessionDetail
	unmarshalStructured(t, result, &getOut)
	assert.Equal(t, sid, getOut.ID)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "list_acp_sessions", map[string]any{})
	require.NoError(t, err)
	var listOut listACPSessionsOut
	unmarshalStructured(t, result, &listOut)
	require.Len(t, listOut.Sessions, 1)

	result, err = callTool(ctxWithAuth(Scope{}), deps, "terminate_acp_session", map[string]any{
		"session_id": sid,
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	require.Len(t, bridge.terminated, 1)
	assert.Equal(t, sid, bridge.terminated[0].String())
}

func TestCreateACPSession_DeniedForSystemToken(t *testing.T) {
	deps, bridge := acpTestDeps()

	// system token（agent 会话签发）→ 不允许套娃创建嵌套 session
	ctx := context.WithValue(context.Background(), AuthContextKey, &AuthSession{
		UserID:  testUID.String(),
		TokenID: uuid.NewString(),
		Issuer:  IssuerSystem,
	})
	result, err := callTool(ctx, deps, "create_acp_session", map[string]any{
		"workspace_id": testWSID.String(), "agent_kind_id": uuid.New().String(),
	})
	require.NoError(t, err)
	assert.True(t, result.IsError)
	assert.Empty(t, bridge.sessions)

	// admin token（Issuer admin）→ 允许
	ctx = context.WithValue(context.Background(), AuthContextKey, &AuthSession{
		UserID:  testUID.String(),
		TokenID: uuid.NewString(),
		Issuer:  IssuerAdmin,
	})
	result, err = callTool(ctx, deps, "create_acp_session", map[string]any{
		"workspace_id": testWSID.String(), "agent_kind_id": uuid.New().String(),
	})
	require.NoError(t, err)
	assert.False(t, result.IsError)
	assert.Len(t, bridge.sessions, 1)
}

func TestListACPSessions_ScopeFiltered(t *testing.T) {
	deps, bridge := acpTestDeps()
	otherProject := uuid.New()
	bridge.sessions = []ACPSession{
		{ID: uuid.New(), WorkspaceID: testWSID, ProjectID: testPID, AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now()},
		{ID: uuid.New(), WorkspaceID: uuid.New(), ProjectID: otherProject, AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now()},
	}

	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
		"list_acp_sessions", map[string]any{})
	require.NoError(t, err)
	var out listACPSessionsOut
	unmarshalStructured(t, result, &out)
	require.Len(t, out.Sessions, 1, "scope 外项目的 session 应被过滤")
	assert.Equal(t, testPID.String(), out.Sessions[0].ProjectID)
}

func TestListPendingPermissions(t *testing.T) {
	deps, bridge := acpTestDeps()
	sid := uuid.New()
	bridge.sessions = []ACPSession{{
		ID: sid, WorkspaceID: testWSID, ProjectID: testPID,
		AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now(),
	}}
	bridge.perms = []ACPPermission{{
		ID: uuid.New(), SessionID: sid, AgentRequestID: "req-1",
		ToolName: "fs/write", Status: "pending", CreatedAt: time.Now(),
	}}

	result, err := callTool(ctxWithAuth(Scope{}), deps, "list_pending_permissions", map[string]any{
		"session_id": sid.String(),
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out listPendingPermissionsOut
	unmarshalStructured(t, result, &out)
	require.Len(t, out.Permissions, 1)
	assert.Equal(t, "fs/write", out.Permissions[0].ToolName)
	assert.Equal(t, "pending", out.Permissions[0].Status)
}

func TestListPendingApprovals(t *testing.T) {
	deps, bridge := acpTestDeps()
	sidA := uuid.New()
	sidB := uuid.New()
	bridge.perms = []ACPPermission{
		{ID: uuid.New(), SessionID: sidA, AgentRequestID: "r1", ToolName: "fs/write", Status: "pending", CreatedAt: time.Now()},
		{ID: uuid.New(), SessionID: sidB, AgentRequestID: "r2", ToolName: "shell/exec", Status: "pending", CreatedAt: time.Now()},
	}

	result, err := callTool(ctxWithAuth(Scope{}), deps, "list_pending_approvals", map[string]any{})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out listPendingPermissionsOut
	unmarshalStructured(t, result, &out)
	// Cross-session: both pending requests returned regardless of session.
	require.Len(t, out.Permissions, 2)
}

func TestGetACPSession_IncludesLastStopReason(t *testing.T) {
	deps, bridge := acpTestDeps()
	sid := uuid.New()
	reason := "end_turn"
	bridge.sessions = []ACPSession{{
		ID: sid, WorkspaceID: testWSID, ProjectID: testPID,
		AgentKindID: uuid.New(), UserID: testUID, Status: "running",
		LastStopReason: &reason, StartedAt: time.Now(),
	}}

	result, err := callTool(ctxWithAuth(Scope{}), deps, "get_acp_session", map[string]any{
		"session_id": sid.String(),
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out acpSessionDetail
	unmarshalStructured(t, result, &out)
	assert.Equal(t, "end_turn", out.LastStopReason)
}

func TestGetACPSessionTurns(t *testing.T) {
	deps, bridge := acpTestDeps()
	sid := uuid.New()
	bridge.sessions = []ACPSession{{
		ID: sid, WorkspaceID: testWSID, ProjectID: testPID,
		AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now(),
	}}
	stop := "end_turn"
	completed := time.Now()
	bridge.turns = map[uuid.UUID][]ACPTurn{
		sid: {
			{TurnIndex: 0, Status: "completed", StopReason: &stop, UpdateCount: 3, StartedAt: time.Now(), CompletedAt: &completed},
			{TurnIndex: 1, Status: "in_progress", UpdateCount: 0, StartedAt: time.Now()},
		},
	}

	result, err := callTool(ctxWithAuth(Scope{}), deps, "get_acp_session_turns", map[string]any{
		"session_id": sid.String(),
	})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out getACPSessionTurnsOut
	unmarshalStructured(t, result, &out)
	require.Len(t, out.Turns, 2)
	assert.Equal(t, 0, out.Turns[0].TurnIndex)
	assert.Equal(t, "completed", out.Turns[0].Status)
	assert.Equal(t, "end_turn", out.Turns[0].StopReason)
	assert.Equal(t, 3, out.Turns[0].UpdateCount)
	assert.Equal(t, "in_progress", out.Turns[1].Status)
	assert.Empty(t, out.Turns[1].StopReason)
}

func TestGetACPSessionTurns_CrossProjectDenied(t *testing.T) {
	deps, bridge := acpTestDeps()
	sid := uuid.New()
	otherProject := uuid.New()
	bridge.sessions = []ACPSession{{
		ID: sid, WorkspaceID: testWSID, ProjectID: otherProject,
		AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now(),
	}}

	// caller scope 限定到 testPID，session 属于 otherProject → 拒绝
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
		"get_acp_session_turns", map[string]any{"session_id": sid.String()})
	require.NoError(t, err)
	assert.True(t, result.IsError, "cross-project access must be denied")
}

func TestGetRunDashboard(t *testing.T) {
	deps, bridge := acpTestDeps()
	day := time.Now().UTC()
	bridge.dashboard = &ACPRunDashboard{
		From: day.Add(-24 * time.Hour), To: day,
		Total: 6, Succeeded: 4, Crashed: 2, Active: 0,
		SuccessRate: 4.0 / 6.0, TotalCostUSD: 2.25, TotalTokens: 1500,
		AvgDurationSeconds: 42.5,
		ByDay: []ACPRunDayRow{
			{Day: day, Total: 6, Succeeded: 4, Crashed: 2, TotalCostUSD: 2.25},
		},
	}

	result, err := callTool(ctxWithAuth(Scope{}), deps, "get_run_dashboard", map[string]any{})
	require.NoError(t, err)
	require.False(t, result.IsError, "body=%v", result)

	var out getRunDashboardOut
	unmarshalStructured(t, result, &out)
	assert.Equal(t, int64(6), out.Total)
	assert.Equal(t, int64(4), out.Succeeded)
	assert.Equal(t, int64(2), out.Crashed)
	assert.InDelta(t, 4.0/6.0, out.SuccessRate, 1e-9)
	assert.InDelta(t, 2.25, out.TotalCostUSD, 1e-9)
	assert.Equal(t, int64(1500), out.TotalTokens)
	assert.InDelta(t, 42.5, out.AvgDurationSeconds, 1e-9)
	require.Len(t, out.ByDay, 1)
	assert.Equal(t, int64(6), out.ByDay[0].Total)
}

func TestGetRunDashboard_PassesFilters(t *testing.T) {
	deps, bridge := acpTestDeps()
	pid := uuid.New()
	reqID := uuid.New()
	from := time.Now().Add(-72 * time.Hour).UTC().Truncate(time.Second)
	to := time.Now().UTC().Truncate(time.Second)

	result, err := callTool(ctxWithAuth(Scope{}), deps, "get_run_dashboard", map[string]any{
		"project_id":     pid.String(),
		"requirement_id": reqID.String(),
		"from":           from.Format(time.RFC3339),
		"to":             to.Format(time.RFC3339),
	})
	require.NoError(t, err)
	require.False(t, result.IsError, "body=%v", result)

	require.NotNil(t, bridge.dashIn.ProjectID)
	assert.Equal(t, pid, *bridge.dashIn.ProjectID)
	require.NotNil(t, bridge.dashIn.RequirementID)
	assert.Equal(t, reqID, *bridge.dashIn.RequirementID)
	assert.True(t, from.Equal(bridge.dashIn.From))
	assert.True(t, to.Equal(bridge.dashIn.To))
}

func TestGetRunDashboard_BadFilter(t *testing.T) {
	deps, _ := acpTestDeps()
	result, err := callTool(ctxWithAuth(Scope{}), deps, "get_run_dashboard", map[string]any{
		"project_id": "not-a-uuid",
	})
	require.NoError(t, err)
	assert.True(t, result.IsError, "invalid project_id must error")
}
