package jin10

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
	"github.com/tidbcloud/AgentTeam/internal/service/extensionflow"
)

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

	quoteCodes := map[string]any{
		"data":   []map[string]any{{"code": "XAUUSD", "name": "Gold"}},
		"status": 200, "message": "ok",
	}
	quote := map[string]any{
		"data": map[string]any{
			"code": "XAUUSD", "name": "Gold", "time": "2026-08-20T00:00:00Z",
			"open": "3300", "close": "3310", "high": "3320", "low": "3290",
			"volume": 42, "ups_price": "10", "ups_percent": "0.3%",
		},
		"status": 200, "message": "ok",
	}
	kline := map[string]any{
		"data": map[string]any{
			"code": "XAUUSD", "name": "Gold",
			"klines": []map[string]any{{
				"time": 1787184000, "open": "3300", "close": "3301",
				"high": "3302", "low": "3299", "volume": 7,
			}},
		},
		"status": 200, "message": "ok",
	}
	flashItem := map[string]any{
		"title": "Flash", "content": "Central bank update",
		"time": "2026-08-20 09:00:00", "url": "https://www.jin10.com/flash/1",
	}
	newsItem := map[string]any{
		"id": "news-1", "title": "Markets", "introduction": "Summary",
		"time": "2026-08-20 09:00:00", "url": "https://www.jin10.com/news/1",
	}
	calendarItem := map[string]any{
		"pub_time": "2026-08-20 20:30:00", "star": 3, "title": "CPI",
		"previous": "2.0", "consensus": "2.1", "actual": nil,
		"revised": nil, "affect_txt": "USD",
	}

	testCases := []struct {
		action       string
		input        string
		method       string
		tool         string
		arguments    string
		output       map[string]any
		textFallback bool
	}{
		{
			action: ActionListQuoteCodes, input: `{}`, method: "resources/read",
			output: quoteCodes,
		},
		{
			action: ActionGetQuote, input: `{"code":"XAUUSD"}`,
			method: "tools/call", tool: "get_quote",
			arguments: `{"code":"XAUUSD"}`, output: quote,
		},
		{
			action: ActionGetKline,
			input:  `{"code":"XAUUSD","time":1787184000,"count":2}`,
			method: "tools/call", tool: "get_kline",
			arguments: `{"code":"XAUUSD","time":1787184000,"count":2}`,
			output:    kline,
		},
		{
			action: ActionListFlash, input: `{"cursor":"next-1"}`,
			method: "tools/call", tool: "list_flash",
			arguments: `{"cursor":"next-1"}`,
			output: map[string]any{
				"data": map[string]any{
					"items":       []map[string]any{flashItem},
					"next_cursor": "next-2", "has_more": true,
				},
				"status": 200, "message": "ok",
			},
		},
		{
			action: ActionSearchFlash, input: `{"keyword":"央行"}`,
			method: "tools/call", tool: "search_flash",
			arguments: `{"keyword":"央行"}`,
			output: map[string]any{
				"data":   map[string]any{"items": []map[string]any{flashItem}},
				"status": 200, "message": "ok",
			},
		},
		{
			action: ActionListNews, input: `{}`,
			method: "tools/call", tool: "list_news", arguments: `{}`,
			output: map[string]any{
				"data": map[string]any{
					"items":       []map[string]any{newsItem},
					"next_cursor": "", "has_more": false,
				},
				"status": 200, "message": "ok",
			},
		},
		{
			action: ActionSearchNews,
			input:  `{"keyword":"黄金","cursor":""}`,
			method: "tools/call", tool: "search_news",
			arguments: `{"keyword":"黄金","cursor":""}`,
			output: map[string]any{
				"data": map[string]any{
					"items":       []map[string]any{newsItem},
					"next_cursor": "", "has_more": false,
				},
				"status": 200, "message": "ok",
			},
			textFallback: true,
		},
		{
			action: ActionGetNews, input: `{"id":"news-1"}`,
			method: "tools/call", tool: "get_news",
			arguments: `{"id":"news-1"}`,
			output: map[string]any{
				"data": map[string]any{
					"id": "news-1", "title": "Markets", "introduction": "Summary",
					"content": "Full article", "time": "2026-08-20 09:00:00",
					"url": "https://www.jin10.com/news/1",
				},
				"status": 200, "message": "ok",
			},
		},
		{
			action: ActionListCalendar, input: `{}`,
			method: "tools/call", tool: "list_calendar", arguments: `{}`,
			output: map[string]any{
				"data":   []map[string]any{calendarItem},
				"status": 200, "message": "ok",
			},
		},
	}

	for _, testCase := range testCases {
		testCase := testCase
		t.Run(testCase.action, func(t *testing.T) {
			t.Parallel()

			expectedOutput := mustJSON(t, testCase.output)
			server := &mcpWireDouble{
				t:          t,
				credential: "test-secret",
			}
			server.operationResult = func(request rpcRequest) any {
				require.Equal(t, testCase.method, request.Method)
				if request.Method == "resources/read" {
					var params struct {
						URI string `json:"uri"`
					}
					require.NoError(t, json.Unmarshal(request.Params, &params))
					require.Equal(t, QuoteCodesResourceURI, params.URI)
					return map[string]any{
						"contents": []map[string]any{{
							"uri": QuoteCodesResourceURI, "mimeType": "application/json",
							"text": string(expectedOutput),
						}},
					}
				}
				var params struct {
					Name      string          `json:"name"`
					Arguments json.RawMessage `json:"arguments"`
				}
				require.NoError(t, json.Unmarshal(request.Params, &params))
				require.Equal(t, testCase.tool, params.Name)
				require.JSONEq(t, testCase.arguments, string(params.Arguments))
				if testCase.textFallback {
					return map[string]any{
						"content": []map[string]any{{
							"type": "text", "text": string(expectedOutput),
						}},
					}
				}
				return map[string]any{
					"content": []map[string]any{{
						"type": "text", "text": "not exposed",
					}},
					"structuredContent": json.RawMessage(expectedOutput),
				}
			}

			client, err := newMCPClient(
				server,
				allowedToolNames(),
				[]string{QuoteCodesResourceURI},
			)
			require.NoError(t, err)
			executor, err := newExecutor(
				staticCredentialSource{
					credentials: runner.Credentials{
						"apiKey": "test-secret",
					},
				},
				client,
			)
			require.NoError(t, err)
			result, err := executor.Invoke(
				context.Background(),
				registry.BuiltinInvocation{
					WorkspaceID:           "workspace-1",
					ServiceInstallationID: "installation-1",
					Action:                testCase.action,
					Input:                 json.RawMessage(testCase.input),
				},
			)
			require.NoError(t, err)
			require.JSONEq(t, string(expectedOutput), string(result.Data))
			require.Equal(t, []string{
				"server/discover",
				"initialize",
				"notifications/initialized",
				testCase.method,
				"DELETE",
			}, server.recordedMethods())
		})
	}
}

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

	for _, testCase := range []struct {
		name   string
		action string
		input  string
		result any
	}{
		{
			name:   "structured non-200",
			action: ActionGetQuote, input: `{"code":"XAUUSD"}`,
			result: map[string]any{
				"structuredContent": json.RawMessage(
					`{"data":null,"status":500,"message":"upstream failed"}`,
				),
			},
		},
		{
			name:   "text non-200",
			action: ActionGetQuote, input: `{"code":"XAUUSD"}`,
			result: map[string]any{
				"content": []map[string]any{{
					"type": "text",
					"text": `{"data":null,"status":401,"message":"unauthorized"}`,
				}},
			},
		},
		{
			name:   "resource non-200",
			action: ActionListQuoteCodes, input: `{}`,
			result: map[string]any{
				"contents": []map[string]any{{
					"uri": QuoteCodesResourceURI,
					"text": `{
						"data":[],
						"status":500,
						"message":"upstream failed"
					}`,
				}},
			},
		},
	} {
		testCase := testCase
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			server := &mcpWireDouble{
				t: t, credential: "test-secret",
				operationResult: func(rpcRequest) any {
					return testCase.result
				},
			}
			client, err := newMCPClient(
				server,
				allowedToolNames(),
				[]string{QuoteCodesResourceURI},
			)
			require.NoError(t, err)
			executor, err := newExecutor(
				staticCredentialSource{
					credentials: runner.Credentials{
						"apiKey": "test-secret",
					},
				},
				client,
			)
			require.NoError(t, err)

			_, err = executor.Invoke(
				context.Background(),
				registry.BuiltinInvocation{
					WorkspaceID:           "workspace-1",
					ServiceInstallationID: "installation-1",
					Action:                testCase.action,
					Input:                 json.RawMessage(testCase.input),
				},
			)
			code, ok := extensionflow.InvocationErrorCodeOf(err)
			require.True(t, ok)
			require.Equal(t, extensionflow.InvocationUpstreamFailure, code)
			require.NotContains(t, err.Error(), "upstream failed")
			require.NotContains(t, err.Error(), "unauthorized")
		})
	}
}

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

	for _, testCase := range []struct {
		name    string
		payload string
		wantErr bool
	}{
		{
			name: "valid non-empty quote codes",
			payload: `{
				"data":[{"code":"XAUUSD","name":"Gold"}],
				"status":200,
				"message":"ok"
			}`,
		},
		{
			name:    "empty quote codes fail closed",
			payload: `{"data":[],"status":200,"message":"ok"}`,
			wantErr: true,
		},
		{
			name:    "malformed quote code fails closed",
			payload: `{"data":[{"code":"XAUUSD"}],"status":200,"message":"ok"}`,
			wantErr: true,
		},
	} {
		testCase := testCase
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			server := &mcpWireDouble{
				t:          t,
				credential: "probe-secret",
			}
			server.operationResult = func(request rpcRequest) any {
				require.Equal(t, "resources/read", request.Method)
				return map[string]any{
					"contents": []map[string]any{{
						"uri":  QuoteCodesResourceURI,
						"text": testCase.payload,
					}},
				}
			}
			client, err := newMCPClient(
				server,
				nil,
				[]string{QuoteCodesResourceURI},
			)
			require.NoError(t, err)
			err = validateCredentials(
				context.Background(),
				client,
				runner.Credentials{"apiKey": "probe-secret"},
			)
			if testCase.wantErr {
				require.Equal(
					t,
					runner.ErrorUpstreamFailure,
					runner.CodeOf(err),
				)
			} else {
				require.NoError(t, err)
			}
			_, toolErr := client.CallTool(
				context.Background(),
				runner.Credentials{"apiKey": "probe-secret"},
				"get_quote",
				map[string]any{"code": "XAUUSD"},
			)
			require.Equal(
				t,
				runner.ErrorValidation,
				runner.CodeOf(toolErr),
			)
			require.Equal(t, []string{
				"server/discover",
				"initialize",
				"notifications/initialized",
				"resources/read",
				"DELETE",
			}, server.recordedMethods())
		})
	}
}
