package aifin

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"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 TestExecutorPinsEndpointBearerToolAndArgumentMapping(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		action    string
		input     string
		path      string
		tool      string
		arguments string
		sse       bool
	}{
		{
			action: ActionGetStockPriceIndicators,
			input:  `{"windcode":"00700.hk,600519.sh","indexes":"最新成交价"}`,
			path:   "/vserver_stock_data/mcp/",
			tool:   "get_stock_price_indicators",
			arguments: `{
				"windcode":"0700.HK,600519.SH",
				"indexes":"最新成交价"
			}`,
		},
		{
			action: ActionGetStockKline,
			input:  `{"windcode":"600519.sh","begin_date":"2026-01-01","end_date":"2026-01-31"}`,
			path:   "/vserver_stock_data/mcp/",
			tool:   "get_stock_kline",
			arguments: `{
				"windcode":"600519.SH",
				"begin_date":"2026-01-01",
				"end_date":"2026-01-31",
				"period":"10"
			}`,
		},
		{
			action:    ActionSearchStocks,
			input:     `{"question":"筛选A股"}`,
			path:      "/vserver_stock_data/mcp/",
			tool:      "search_stocks",
			arguments: `{"question":"筛选A股"}`,
		},
		{
			action:    ActionGetStockFundamentals,
			input:     `{"question":"贵州茅台净利润"}`,
			path:      "/vserver_stock_data/mcp/",
			tool:      "get_stock_fundamentals",
			arguments: `{"question":"贵州茅台净利润"}`,
		},
		{
			action:    ActionGetFundPriceIndicators,
			input:     `{"windcode":"005827.of"}`,
			path:      "/vserver_fund_data/mcp/",
			tool:      "get_fund_price_indicators",
			arguments: `{"windcode":"005827.OF"}`,
		},
		{
			action: ActionGetFundKline,
			input:  `{"windcode":"588200.SH","begin_date":"2026-01-01","end_date":"2026-01-31","period":"1w","aftype":"1","issusp":"0"}`,
			path:   "/vserver_fund_data/mcp/",
			tool:   "get_fund_kline",
			arguments: `{
				"windcode":"588200.SH",
				"begin_date":"2026-01-01",
				"end_date":"2026-01-31",
				"period":"11",
				"aftype":"1",
				"issusp":"0"
			}`,
		},
		{
			action:    ActionSearchFunds,
			input:     `{"question":"筛选股票型基金"}`,
			path:      "/vserver_fund_data/mcp/",
			tool:      "search_funds",
			arguments: `{"question":"筛选股票型基金"}`,
		},
		{
			action:    ActionGetFundPerformance,
			input:     `{"question":"易方达蓝筹精选最大回撤"}`,
			path:      "/vserver_fund_data/mcp/",
			tool:      "get_fund_performance",
			arguments: `{"question":"易方达蓝筹精选最大回撤"}`,
		},
		{
			action:    ActionGetIndexPriceIndicators,
			input:     `{"windcode":"000300.sh"}`,
			path:      "/vserver_index_data/mcp/",
			tool:      "get_index_price_indicators",
			arguments: `{"windcode":"000300.SH"}`,
		},
		{
			action: ActionGetIndexKline,
			input:  `{"windcode":"000300.SH","begin_date":"2026-01-01","end_date":"2026-01-31","period":"5min"}`,
			path:   "/vserver_index_data/mcp/",
			tool:   "get_index_kline",
			arguments: `{
				"windcode":"000300.SH",
				"begin_date":"2026-01-01",
				"end_date":"2026-01-31",
				"period":"3"
			}`,
		},
		{
			action:    ActionGetCompanyAnnouncements,
			input:     `{"query":"贵州茅台2025年年度报告","top_k":3}`,
			path:      "/vserver_financial_docs/mcp/",
			tool:      "get_company_announcements",
			arguments: `{"query":"贵州茅台2025年年度报告","top_k":3}`,
		},
		{
			action:    ActionGetFinancialNews,
			input:     `{"query":"美联储利率政策"}`,
			path:      "/vserver_financial_docs/mcp/",
			tool:      "get_financial_news",
			arguments: `{"query":"美联储利率政策"}`,
			sse:       true,
		},
		{
			action: ActionNaturalLanguageGetEDBData,
			input:  `{"executionMode":"搜索并提数","question":"中国GDP","observation":"10"}`,
			path:   "/vserver_economic_data/mcp/",
			tool:   "natural_language_get_edb_data",
			arguments: `{
				"executionMode":"searchFetch",
				"question":"中国GDP",
				"observation":"10"
			}`,
		},
	}

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

			server := &windMCPWireDouble{
				t:            t,
				credential:   "wind-test-secret",
				path:         testCase.path,
				sseOperation: testCase.sse,
			}
			server.result = func(request windRPCRequest) any {
				require.Equal(t, "tools/call", request.Method)
				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),
				)
				return map[string]any{
					"content": []map[string]any{{
						"type": "text",
						"text": "must not be exposed",
					}},
					"structuredContent": map[string]any{
						"source": "wind",
						"rows":   []any{},
					},
				}
			}
			clients, err := newMCPClients(server)
			require.NoError(t, err)
			executor, err := newExecutor(
				staticCredentialSource{
					credentials: runner.Credentials{
						"apiKey": "wind-test-secret",
					},
				},
				clients,
			)
			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,
				`{"result":{"source":"wind","rows":[]}}`,
				string(result.Data),
			)
			require.Equal(t, []string{
				"server/discover",
				"initialize",
				"notifications/initialized",
				"tools/call",
				"DELETE",
			}, server.recordedMethods())
		})
	}
}

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

	server := &windMCPWireDouble{
		t:          t,
		credential: "wind-test-secret",
		path:       "/vserver_stock_data/mcp/",
		result: func(windRPCRequest) any {
			return map[string]any{
				"content": []map[string]any{{
					"type": "text",
					"text": `{"rows":[1]}`,
				}},
			}
		},
	}
	executor := newWireExecutor(t, server)
	result, err := executor.Invoke(
		context.Background(),
		registry.BuiltinInvocation{
			WorkspaceID:           "workspace-1",
			ServiceInstallationID: "installation-1",
			Action:                ActionSearchStocks,
			Input:                 json.RawMessage(`{"question":"A股"}`),
		},
	)
	require.NoError(t, err)
	require.JSONEq(t, `{"result":{"rows":[1]}}`, string(result.Data))
}

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

	for _, testCase := range []struct {
		name    string
		code    any
		message string
	}{
		{name: "OK", code: 200, message: "OK"},
		{name: "SUCCESS", code: 201, message: "SUCCESS"},
	} {
		testCase := testCase
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			server := &windMCPWireDouble{
				t:          t,
				credential: "wind-test-secret",
				path:       "/vserver_stock_data/mcp/",
				result: func(windRPCRequest) any {
					return map[string]any{
						"structuredContent": map[string]any{
							"data": map[string]any{
								"code":    testCase.code,
								"message": testCase.message,
								"data":    []any{},
							},
						},
					}
				},
			}
			executor := newWireExecutor(t, server)
			result, err := executor.Invoke(
				context.Background(),
				registry.BuiltinInvocation{
					WorkspaceID:           "workspace-1",
					ServiceInstallationID: "installation-1",
					Action:                ActionSearchStocks,
					Input:                 json.RawMessage(`{"question":"A股"}`),
				},
			)
			require.NoError(t, err)
			require.JSONEq(
				t,
				fmt.Sprintf(
					`{"result":{"data":{"code":%v,"message":%q,"data":[]}}}`,
					testCase.code,
					testCase.message,
				),
				string(result.Data),
			)
		})
	}
}

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

	for _, representation := range []string{"1", "1.0", "1e0"} {
		representation := representation
		t.Run(representation, func(t *testing.T) {
			t.Parallel()

			server := &windMCPWireDouble{
				t:          t,
				credential: "wind-test-secret",
				path:       "/vserver_financial_docs/mcp/",
			}
			server.result = func(request windRPCRequest) any {
				var params struct {
					Name      string          `json:"name"`
					Arguments json.RawMessage `json:"arguments"`
				}
				require.NoError(t, json.Unmarshal(request.Params, &params))
				require.Equal(t, "get_financial_news", params.Name)
				require.JSONEq(
					t,
					`{"query":"新闻","top_k":1}`,
					string(params.Arguments),
				)
				return map[string]any{
					"structuredContent": map[string]any{
						"rows": []any{},
					},
				}
			}
			executor := newWireExecutor(t, server)
			result, err := executor.Invoke(
				context.Background(),
				registry.BuiltinInvocation{
					WorkspaceID:           "workspace-1",
					ServiceInstallationID: "installation-1",
					Action:                ActionGetFinancialNews,
					Input: json.RawMessage(
						`{"query":"新闻","top_k":` +
							representation +
							`}`,
					),
				},
			)
			require.NoError(t, err)
			require.JSONEq(t, `{"result":{"rows":[]}}`, string(result.Data))
		})
	}
}

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

	testCases := []struct {
		name   string
		result any
	}{
		{
			name: "is error",
			result: map[string]any{
				"isError": true,
				"content": []map[string]any{{
					"type": "text",
					"text": `{"error":{"message":"failed"}}`,
				}},
			},
		},
		{
			name: "interactive",
			result: map[string]any{
				"resultType":    "input_required",
				"inputRequests": map[string]any{},
			},
		},
		{
			name: "nested MCP envelope",
			result: map[string]any{
				"payload": map[string]any{
					"isError": true,
					"content": []map[string]any{{
						"type": "text",
						"text": "failed",
					}},
				},
			},
		},
		{
			name: "nested Wind error",
			result: map[string]any{
				"rows": []map[string]any{{
					"mcp_tool_error_code": 1001,
				}},
			},
		},
		{
			name: "non-empty input requests",
			result: map[string]any{
				"inputRequests": map[string]any{
					"confirm": map[string]any{},
				},
				"rows": []map[string]any{},
			},
		},
		{
			name: "numeric string Wind business error",
			result: map[string]any{
				"structuredContent": map[string]any{
					"data": map[string]any{
						"code":    "1003",
						"message": "not found",
						"data":    []any{},
					},
				},
			},
		},
	}
	for _, testCase := range testCases {
		testCase := testCase
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			server := &windMCPWireDouble{
				t:          t,
				credential: "wind-test-secret",
				path:       "/vserver_stock_data/mcp/",
				result: func(windRPCRequest) any {
					return testCase.result
				},
			}
			executor := newWireExecutor(t, server)
			_, err := executor.Invoke(
				context.Background(),
				registry.BuiltinInvocation{
					WorkspaceID:           "workspace-1",
					ServiceInstallationID: "installation-1",
					Action:                ActionSearchStocks,
					Input:                 json.RawMessage(`{"question":"A股"}`),
				},
			)
			code, ok := extensionflow.InvocationErrorCodeOf(err)
			require.True(t, ok)
			require.Equal(
				t,
				extensionflow.InvocationUpstreamFailure,
				code,
			)
		})
	}
}

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

	server := &windMCPWireDouble{
		t:          t,
		credential: "never-used",
		path:       "/never-used",
	}
	clients, err := newMCPClients(server)
	require.NoError(t, err)
	executor, err := newExecutor(
		staticCredentialSource{
			err: errors.New("database contained wind-secret"),
		},
		clients,
	)
	require.NoError(t, err)
	_, err = executor.Invoke(
		context.Background(),
		registry.BuiltinInvocation{
			WorkspaceID:           "workspace-1",
			ServiceInstallationID: "installation-1",
			Action:                "call-tool",
			Input:                 json.RawMessage(`{}`),
		},
	)
	code, ok := extensionflow.InvocationErrorCodeOf(err)
	require.True(t, ok)
	require.Equal(t, extensionflow.InvocationNotFound, code)
	require.Empty(t, server.recordedMethods())
	require.NotContains(t, err.Error(), "wind-secret")
}

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

	server := &windMCPWireDouble{
		t:          t,
		credential: "never-used",
		path:       "/never-used",
	}
	clients, err := newMCPClients(server)
	require.NoError(t, err)
	executor, err := newExecutor(
		staticCredentialSource{
			err: errors.New("database contained wind-secret"),
		},
		clients,
	)
	require.NoError(t, err)
	_, err = executor.Invoke(
		context.Background(),
		registry.BuiltinInvocation{
			WorkspaceID:           "workspace-1",
			ServiceInstallationID: "installation-1",
			Action:                ActionSearchStocks,
			Input:                 json.RawMessage(`{"question":"A股"}`),
		},
	)
	code, ok := extensionflow.InvocationErrorCodeOf(err)
	require.True(t, ok)
	require.Equal(
		t,
		extensionflow.InvocationProviderAuthUnavailable,
		code,
	)
	require.Empty(t, server.recordedMethods())
	require.NotContains(t, err.Error(), "wind-secret")
}

func newWireExecutor(
	t *testing.T,
	server *windMCPWireDouble,
) *Executor {
	t.Helper()
	clients, err := newMCPClients(server)
	require.NoError(t, err)
	executor, err := newExecutor(
		staticCredentialSource{
			credentials: runner.Credentials{
				"apiKey": "wind-test-secret",
			},
		},
		clients,
	)
	require.NoError(t, err)
	return executor
}
