package dispatcher

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

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

	"github.com/tidbcloud/AgentTeam/desktop/internal/api"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/mcp"
	"github.com/tidbcloud/AgentTeam/desktop/internal/streamregistry"
	"github.com/tidbcloud/AgentTeam/desktop/internal/token"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/internal/uisession"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

// e2eConnection is a minimal ServerConnection returning a stub APIClient so
// the in-scope message_send can actually send instead of panicking on nil.
type e2eConnection struct{}

func (e2eConnection) ID() string                      { return "agentteam-main" }
func (e2eConnection) Enabled() bool                   { return true }
func (e2eConnection) Start(ctx context.Context) error { return nil }
func (e2eConnection) Stop(ctx context.Context) error  { return nil }

func (e2eConnection) Send(ctx context.Context, meta types.Metadata, msg types.OutboundMessage) error {
	return nil
}

var e2eTransportHits int
var e2eSendPathHits int

// APIClient satisfies mcp.AgentTeamConnection with a working stub client so the
// in-scope message_send completes a real HTTP round trip without panicking.
func (e2eConnection) APIClient(agentID ...string) *apiclient.Client {
	return &apiclient.Client{
		ServerURL:        "http://agentteam.invalid",
		AgentID:          "server-agent-fixture",
		AuthToken:        "token",
		RouteWorkspaceID: "wsp_fixture",
		HTTPClient:       &http.Client{Transport: e2eRoundTripper{}},
	}
}

type e2eRoundTripper struct{}

func (e2eRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
	e2eTransportHits++
	var body string
	switch {
	case strings.HasSuffix(r.URL.Path, "/send"):
		e2eSendPathHits++
		body = `{"state":"sent","messageId":"m1","targetKey":"k","acceptedSeq":1,"seenUpToSeq":1}`
	case strings.HasSuffix(r.URL.Path, "/resolve-channel"):
		body = `{"scopeId":"s","targetKey":"k"}`
	default:
		body = `{}`
	}
	return &http.Response{
		StatusCode: http.StatusOK,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       io.NopCloser(strings.NewReader(body)),
		Request:    r,
	}, nil
}

type e2eChannels struct {
	conn types.ServerConnection
}

func (c e2eChannels) ServerConnection(id string) (types.ServerConnection, bool) {
	if id == "agentteam-main" {
		return c.conn, true
	}
	return nil, false
}
func (c e2eChannels) ServerConnections() []types.ServerConnection {
	return []types.ServerConnection{c.conn}
}
func (c e2eChannels) Enable(id string) error  { return nil }
func (c e2eChannels) Disable(id string) error { return nil }

// TestMCPRouteMessageSendGuardEndToEnd proves the full production chain with
// no production test seams: a real Dispatcher bridges a live fixture Process
// through the registered /api/internal/mcp route and the real AccessControl
// middleware into the message_send guard, with a working transport so the
// in-scope send completes for real.
func TestMCPRouteMessageSendGuardEndToEnd(t *testing.T) {
	gin.SetMode(gin.TestMode)
	d, err := New("", Dependencies{})
	require.NoError(t, err)

	process := NewDispatcherScopeFixture(t)
	raw, err := token.Default().GenerateAgentTokenForServer("agent-guard", "agentteam-main")
	require.NoError(t, err)
	sess := InteractiveSessionFixture("agent-guard", raw, process)
	d.sessionManager.Set("agent-guard", sess)

	sessions := e2eSessions{Dispatcher: d}
	handler, err := api.NewRouterWithOptions(
		e2eChannels{conn: e2eConnection{}},
		sessions,
		noTrajectories{},
		uisession.NewStore(uisession.Options{}),
		streamregistry.New(),
		api.RouterOptions{MessageService: mcp.NewMessageService()},
	)
	require.NoError(t, err)

	postCall := func(body, tokenValue string) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPost, "/api/internal/mcp/servers/agentteam-main/agents/agent-guard", strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+tokenValue)
		rec := httptest.NewRecorder()
		handler.ServeHTTP(rec, req)
		return rec
	}

	hitsBefore := e2eTransportHits
	inScope := postCall(`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"message_send","arguments":{"target":"#fixture-turn","content":"hi"}}}`, raw)
	require.Equal(t, http.StatusOK, inScope.Code, "no 5xx/panic on the in-scope send")
	require.NotContains(t, inScope.Body.String(), "isn't active in this turn")
	require.Greater(t, e2eTransportHits, hitsBefore, "the guard must have let the send reach the transport")
	require.NotZero(t, e2eSendPathHits, "the in-scope send must hit the real /send path")
	require.Contains(t, inScope.Body.String(), "sent", "the tool result must report the send outcome")

	outOfScope := postCall(`{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"message_send","arguments":{"target":"#other-turn","content":"hi"}}}`, raw)
	require.Contains(t, outOfScope.Body.String(), "isn't active in this turn")

	other, err := token.Default().GenerateAgentTokenForServer("agent-other", "agentteam-main")
	require.NoError(t, err)
	wrongBearer := postCall(`{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"message_send","arguments":{"target":"#fixture-turn","content":"hi"}}}`, other)
	require.NotEqual(t, http.StatusOK, wrongBearer.Code)
}

type noTrajectories struct{}

func (noTrajectories) Session(string) (types.SessionSnapshot, bool) {
	return types.SessionSnapshot{}, false
}
func (noTrajectories) SubscribeTrajectory(string) <-chan types.TrajectoryLogItem {
	return nil
}
func (noTrajectories) SubscribeLiveTrajectory(string) <-chan types.TrajectoryLogItem {
	return nil
}
func (noTrajectories) UnsubscribeTrajectory(string, <-chan types.TrajectoryLogItem) {}
func (noTrajectories) TrajectoryHistory(string, int, int64) []types.TrajectoryLogItem {
	return nil
}
func (noTrajectories) SubscribeAllLiveTrajectory() <-chan types.TrajectoryLogItem {
	return nil
}
func (noTrajectories) UnsubscribeAllLiveTrajectory(<-chan types.TrajectoryLogItem) {}
func (noTrajectories) UnsubscribeAllTrajectory(<-chan types.TrajectoryLogItem)     {}
func (noTrajectories) Sessions() []types.SessionSnapshot                           { return nil }
func (noTrajectories) LatestTrajectoryByAgent([]string) map[string]types.TrajectoryLogItem {
	return nil
}

// e2eSessions adapts the real Dispatcher with the KB route surface the
// factory also requires (stubbed; not under test).
type e2eSessions struct {
	*Dispatcher
}

func (e2eSessions) CurrentTurnMessageID(string) string { return "" }
