package mcp

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/messagesend"
	"github.com/tidbcloud/AgentTeam/desktop/internal/token"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

type crossTargetRuntime struct {
	serviceTestMessageRuntime
	scope messagesend.Scope
	ok    bool
}

func (r crossTargetRuntime) SendScope(context.Context, string) (messagesend.Scope, bool) {
	return r.scope, r.ok
}

type crossTargetSendTransport struct {
	sends int
}

func (t *crossTargetSendTransport) RoundTrip(request *http.Request) (*http.Response, error) {
	if strings.Contains(request.URL.Path, "/resolve-channel") {
		return messageSendJSONResponse(`{"scopeId":"chn_ops","targetKey":"chn_ops"}`), nil
	}
	t.sends++
	return messageSendJSONResponse(`{"state":"sent","messageId":"m1","targetKey":"chn_ops","acceptedSeq":1,"seenUpToSeq":1}`), nil
}

func newCrossTargetSendHandler(t *testing.T, scope messagesend.Scope, ok bool) (*mcpHandler, *crossTargetSendTransport) {
	t.Helper()
	transport := &crossTargetSendTransport{}
	runtime := crossTargetRuntime{scope: scope, ok: ok}
	handler, _ := newPartialSendTestHandlerWithRuntime(
		t,
		NewMessageService(),
		transport,
		runtime,
	)
	handler.sendScope = runtime
	return handler, transport
}

func scopeOf(targets ...messagesend.Target) messagesend.Scope {
	return messagesend.Scope{Targets: targets}
}

func callCrossTargetSend(t *testing.T, handler *mcpHandler, target string, allowCrossTarget bool) *types.MCPToolResult {
	t.Helper()
	arguments, err := json.Marshal(map[string]any{
		"target":             target,
		"content":            "hello",
		"allow_cross_target": allowCrossTarget,
	})
	require.NoError(t, err)
	return handler.toolSendMessage(context.Background(), "agt-1", arguments)
}

func TestSendMessageRejectsCrossTargetByDefault(t *testing.T) {
	handler, transport := newCrossTargetSendHandler(t, scopeOf(
		messagesend.Target{Target: "dm:@alice", Source: "conversation"},
	), true)

	result := callCrossTargetSend(t, handler, "#agentteam-overnight", false)

	require.True(t, result.IsError)
	require.Contains(t, result.Content[0].Text, "isn't active in this turn")
	require.Contains(t, result.Content[0].Text, `"dm:@alice"`)
	require.Contains(t, result.Content[0].Text, "No message was sent")
	require.Equal(t, 0, transport.sends)
	structured, ok := result.StructuredContent.(messageTargetNotActiveStructuredContent)
	require.True(t, ok)
	require.Equal(t, "target_not_active", structured.Code)
	require.Equal(t, "#agentteam-overnight", structured.RequestedTarget)
	require.Equal(t, []string{"dm:@alice"}, structured.ActiveTargets)
}

func TestSendMessageDenialNamesActiveTargetsAndDeliberateOptIn(t *testing.T) {
	handler, _ := newCrossTargetSendHandler(t, scopeOf(
		messagesend.Target{Target: "dm:@alice", Source: "conversation"},
	), true)

	result := callCrossTargetSend(t, handler, "#agentteam-overnight", false)

	require.True(t, result.IsError)
	// Lead with the outcome, then the reason, then the remedies.
	require.True(t, strings.HasPrefix(result.Content[0].Text, "Error: No message was sent."))
	require.Contains(t, result.Content[0].Text, "dm:@alice")
	// The denial must describe the intentional opt-in, not advertise it as a
	// routine bypass.
	require.Contains(t, result.Content[0].Text, "isn't active in this turn")
	require.Contains(t, result.Content[0].Text, "intentionally")
	require.Contains(t, result.Content[0].Text, "allow_cross_target")
}

func TestSendMessageAllowsDeliveredConversationTarget(t *testing.T) {
	handler, transport := newCrossTargetSendHandler(t, scopeOf(
		messagesend.Target{Target: "#agentteam-in-agentteam:52a2a221", Source: "conversation"},
		messagesend.Target{Target: "dm:@alice", Source: "conversation"},
	), true)

	result := callCrossTargetSend(t, handler, "dm:@alice", false)

	require.False(t, result.IsError)
	require.Equal(t, 1, transport.sends)
}

func TestSendMessageTaskTargetRequiresExplicitCrossTarget(t *testing.T) {
	handler, transport := newCrossTargetSendHandler(t, scopeOf(
		messagesend.Target{Target: "#team", Source: "conversation"},
	), true)

	// Task targets are no longer implicitly authorized by task_dispatch
	// deliveries; they behave like any other out-of-scope target.
	denied := callCrossTargetSend(t, handler, "#tsk_abc", false)
	require.True(t, denied.IsError)
	require.Equal(t, 0, transport.sends)

	forced := callCrossTargetSend(t, handler, "#tsk_abc", true)
	require.False(t, forced.IsError)
	require.Equal(t, 1, transport.sends)
}

func TestSendMessageAllowCrossTargetForcesDelivery(t *testing.T) {
	handler, transport := newCrossTargetSendHandler(t, scopeOf(
		messagesend.Target{Target: "dm:@alice", Source: "conversation"},
	), true)

	result := callCrossTargetSend(t, handler, "#agentteam-overnight", true)

	require.False(t, result.IsError)
	require.Equal(t, 1, transport.sends)
}

func TestSendMessageEmptyScopeAllowsProactiveDestination(t *testing.T) {
	handler, transport := newCrossTargetSendHandler(t, scopeOf(), true)

	result := callCrossTargetSend(t, handler, "#agentteam-overnight", false)

	require.False(t, result.IsError)
	require.Equal(t, 1, transport.sends)
}

func TestSendMessageFailsClosedWhenScopeUnavailable(t *testing.T) {
	handler, transport := newCrossTargetSendHandler(t, scopeOf(), false)

	result := callCrossTargetSend(t, handler, "#agentteam-overnight", false)

	require.True(t, result.IsError)
	require.Contains(t, result.Content[0].Text, "runtime scope unavailable")
	require.Contains(t, result.Content[0].Text, "No message was sent")
	require.Equal(t, 0, transport.sends)
}

// TestAccessControlInjectsBearerIdentityForGuard proves the HTTP middleware →
// bearer identity → guard positive chain end to end: a request bearing the
// agent token passes access control and downstream code reads exactly that
// bearer's identity from the request context via messagesend.BearerID.
func TestAccessControlInjectsBearerIdentityForGuard(t *testing.T) {
	gin.SetMode(gin.TestMode)
	manager := token.Default()
	raw, err := manager.GenerateAgentTokenForServer("agent-http", "server-x")
	require.NoError(t, err)

	observed := make(chan string, 1)
	router := gin.New()
	router.POST("/agents/:agentId/mcp", AccessControl(), func(c *gin.Context) {
		identity, ok := messagesend.BearerID(c.Request.Context())
		require.True(t, ok, "downstream handler must read the injected identity")
		observed <- identity
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodPost, "/agents/agent-http/mcp", nil)
	req.Header.Set("Authorization", "Bearer "+raw)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code, "authenticated request must pass access control")
	require.Equal(t, messagesend.TokenIdentity(raw), <-observed,
		"the guard-visible identity must be the authenticated bearer's")

	// A wrong-session token is rejected before any identity exists.
	router2 := gin.New()
	router2.POST("/agents/:agentId/mcp", AccessControl(), func(c *gin.Context) {
		t.Error("unauthorized request must not reach the handler")
	})
	other, err2 := manager.GenerateAgentTokenForServer("agent-other", "server-x")
	require.NoError(t, err2)
	req2 := httptest.NewRequest(http.MethodPost, "/agents/agent-http/mcp", nil)
	req2.Header.Set("Authorization", "Bearer "+other)
	recorder2 := httptest.NewRecorder()
	router2.ServeHTTP(recorder2, req2)
	require.NotEqual(t, http.StatusOK, recorder2.Code)
}

// dispatcherSendScopeStub adapts a Dispatcher-like function to the provider
// port so the handler guard can consume it in the HTTP chain test.
type dispatcherSendScopeStub struct {
	scope    messagesend.Scope
	ok       bool
	identity string
}

func (s dispatcherSendScopeStub) SendScope(ctx context.Context, agentID string) (messagesend.Scope, bool) {
	identity, bound := messagesend.BearerID(ctx)
	if !bound || identity != s.identity {
		return messagesend.Scope{}, false
	}
	return s.scope, s.ok
}

// TestHTTPChainMiddlewareToGuard proves the real positive chain: the HTTP
// access-control middleware injects the authenticated bearer identity, and the
// message_send guard consumes that same identity through the provider port to
// authorize exactly the delivered target.
func TestHTTPChainMiddlewareToGuard(t *testing.T) {
	gin.SetMode(gin.TestMode)
	manager := token.Default()
	raw, err := manager.GenerateAgentTokenForServer("agent-http-chain", "server-x")
	require.NoError(t, err)

	stub := &dispatcherSendScopeStub{
		scope: messagesend.Scope{Targets: []messagesend.Target{
			{Target: "#delivered-turn", Source: "conversation"},
		}},
		ok: true,
	}
	stub.identity = messagesend.TokenIdentity(raw)

	transport := &crossTargetSendTransport{}
	handler, _ := newPartialSendTestHandlerWithRuntime(
		t, NewMessageService(), transport, crossTargetRuntime{},
	)
	handler.sendScope = stub

	var guardResult *types.MCPToolResult
	router := gin.New()
	router.POST("/agents/:agentId/mcp", AccessControl(), func(c *gin.Context) {
		arguments, _ := json.Marshal(map[string]any{
			"target":  "#delivered-turn",
			"content": "hello",
		})
		guardResult = handler.toolSendMessage(c.Request.Context(), "agent-http-chain", arguments)
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodPost, "/agents/agent-http-chain/mcp", nil)
	req.Header.Set("Authorization", "Bearer "+raw)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)
	require.NotNil(t, guardResult)
	require.False(t, guardResult.IsError, "the delivered target must pass the guard through the real middleware chain")
	require.Equal(t, 1, transport.sends)

	// A target outside the delivered scope is rejected through the same chain.
	arguments, _ := json.Marshal(map[string]any{
		"target":  "#other-turn",
		"content": "hello",
	})
	denied := handler.toolSendMessage(messagesend.WithBearerID(context.Background(), raw), "agent-http-chain", arguments)
	require.True(t, denied.IsError)
	require.Contains(t, denied.Content[0].Text, "isn't active in this turn")
}
