package financialmodelingprep_test

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/stretchr/testify/require"

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

// wireServer records every request and answers with the scripted body for
// every call.
type wireServer struct {
	server   *httptest.Server
	requests []wireRequest
	body     string
	status   int
}

type wireRequest struct {
	method string
	path   string
	query  string
}

func newWireServer(t *testing.T, body string) *wireServer {
	t.Helper()
	wire := &wireServer{body: body}
	wire.server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		read, err := io.ReadAll(r.Body)
		require.NoError(t, err)
		require.Empty(t, read)
		wire.requests = append(wire.requests, wireRequest{method: r.Method, path: r.URL.Path, query: r.URL.RawQuery})
		w.Header().Set("Content-Type", "application/json")
		if wire.status != 0 {
			w.WriteHeader(wire.status)
		}
		_, _ = w.Write([]byte(body))
	}))
	t.Cleanup(wire.server.Close)
	return wire
}

func (wire *wireServer) last() wireRequest {
	if len(wire.requests) == 0 {
		panic("no recorded request")
	}
	return wire.requests[len(wire.requests)-1]
}

type staticCredentialSource struct {
	credentials runner.Credentials
}

func (source staticCredentialSource) ResolveProviderCredentials(
	context.Context, string, string,
) (runner.Credentials, error) {
	return source.credentials, nil
}

func insecureTransport() *http.Transport {
	return &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
}

func invoke(t *testing.T, server *wireServer, action string, input string) (json.RawMessage, error) {
	t.Helper()
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:          server.server.URL,
		Auth:             financialmodelingprep.AuthSpec(),
		StatusClassifier: financialmodelingprep.ClassifyStatus,
		Transport:        insecureTransport(),
	})
	require.NoError(t, err)
	executor := runner.NewRESTExecutor(
		financialmodelingprep.Domain,
		client,
		staticCredentialSource{credentials: runner.Credentials{financialmodelingprep.APIKeyField: "key-1"}},
		financialmodelingprep.Handlers(),
	)
	result, err := executor.Invoke(context.Background(), registry.BuiltinInvocation{
		WorkspaceID:           "ws",
		ServiceInstallationID: "inst",
		ExtensionName:         financialmodelingprep.Domain,
		Action:                action,
		Input:                 json.RawMessage(input),
	})
	if err != nil {
		return nil, err
	}
	return result.Data, nil
}

func classifiedCode(t *testing.T, err error) extensionflow.InvocationErrorCode {
	t.Helper()
	code, ok := extensionflow.InvocationErrorCodeOf(err)
	require.True(t, ok, "error was not classified: %v", err)
	return code
}

// requireWire asserts the recorded request hit GET path with the expected
// query parameters (apikey always present).
func requireWire(t *testing.T, wire *wireServer, path string, params map[string]string) {
	t.Helper()
	request := wire.last()
	require.Equal(t, "GET", request.method)
	require.Equal(t, path, request.path)
	values, err := url.ParseQuery(request.query)
	require.NoError(t, err)
	require.Equal(t, "key-1", values.Get("apikey"))
	for name, value := range params {
		require.Equal(t, value, values.Get(name), "query param %q", name)
	}
}

// Every action's request shape against the double: the route, the mapped
// query parameters, and a well-formed response.
func TestWireEveryActionRequestShape(t *testing.T) {
	rows := `[{"symbol":"AAPL","name":"Apple Inc.","currency":"USD","stockExchange":"NASDAQ","exchangeShortName":"NASDAQ","price":189.5,"date":"2024-01-05","open":180.0,"high":181.5,"low":179.0,"close":181.0,"adjClose":181.0,"volume":52}]`
	cases := []struct {
		action string
		input  string
		path   string
		params map[string]string
	}{
		{"search-symbols", `{"query":"apple","limit":10}`, "/search-symbol", map[string]string{"query": "apple", "limit": "10"}},
		{"search-names", `{"query":"apple"}`, "/search-name", map[string]string{"query": "apple"}},
		{"search-company-screener", `{"marketCapMoreThan":1000000,"sector":"Tech"}`, "/company-screener", map[string]string{"marketCapMoreThan": "1000000", "sector": "Tech"}},
		{"list-directory", `{"type":"stocks"}`, "/stock-list", map[string]string{}},
		{"get-quote", `{"symbol":"AAPL"}`, "/quote", map[string]string{"symbol": "AAPL"}},
		{"get-quote-short", `{"symbol":"AAPL"}`, "/quote-short", map[string]string{"symbol": "AAPL"}},
		{"get-asset-quote", `{"assetType":"crypto","symbol":"BTCUSD"}`, "/batch-crypto-quotes", map[string]string{}},
		{"get-historical-prices", `{"symbol":"AAPL","from":"2024-01-01"}`, "/historical-price-eod/full", map[string]string{"symbol": "AAPL", "from": "2024-01-01"}},
		{"get-intraday-prices", `{"symbol":"AAPL","interval":"5min"}`, "/historical-chart/5min", map[string]string{"symbol": "AAPL"}},
		{"get-company-profile", `{"symbol":"AAPL"}`, "/profile", map[string]string{"symbol": "AAPL"}},
		{"get-company-profile-by-cik", `{"cik":"0000320193"}`, "/profile-cik", map[string]string{"cik": "0000320193"}},
		{"get-company-peers", `{"symbol":"AAPL"}`, "/stock-peers", map[string]string{"symbol": "AAPL"}},
		{"get-company-executives", `{"symbol":"AAPL"}`, "/key-executives", map[string]string{"symbol": "AAPL"}},
		{"get-company-notes", `{"symbol":"AAPL"}`, "/company-notes", map[string]string{"symbol": "AAPL"}},
		{"get-market-cap", `{"symbol":"AAPL"}`, "/market-capitalization", map[string]string{"symbol": "AAPL"}},
		{"get-shares-float", `{"symbol":"AAPL"}`, "/shares-float", map[string]string{"symbol": "AAPL"}},
		{"get-income-statement", `{"symbol":"AAPL","period":"annual"}`, "/income-statement", map[string]string{"symbol": "AAPL", "period": "annual"}},
		{"get-balance-sheet-statement", `{"symbol":"AAPL"}`, "/balance-sheet-statement", map[string]string{"symbol": "AAPL"}},
		{"get-cash-flow-statement", `{"symbol":"AAPL"}`, "/cash-flow-statement", map[string]string{"symbol": "AAPL"}},
		{"get-financial-statement-growth", `{"statementType":"income","symbol":"AAPL"}`, "/income-statement-growth", map[string]string{"symbol": "AAPL"}},
		{"get-financial-ratios", `{"symbol":"AAPL"}`, "/ratios", map[string]string{"symbol": "AAPL"}},
		{"get-key-metrics", `{"symbol":"AAPL"}`, "/key-metrics", map[string]string{"symbol": "AAPL"}},
		{"get-financial-scores", `{"symbol":"AAPL"}`, "/financial-scores", map[string]string{"symbol": "AAPL"}},
		{"get-enterprise-values", `{"symbol":"AAPL"}`, "/enterprise-values", map[string]string{"symbol": "AAPL"}},
		{"get-dcf", `{"symbol":"AAPL"}`, "/discounted-cash-flow", map[string]string{"symbol": "AAPL"}},
		{"get-market-movers", `{"type":"gainers"}`, "/biggest-gainers", map[string]string{}},
		{"get-market-performance", `{"type":"sector_performance"}`, "/sector-performance-snapshot", map[string]string{}},
		{"get-news", `{"type":"stock","symbols":"AAPL"}`, "/news/stock", map[string]string{"symbols": "AAPL"}},
		{"get-news", `{"type":"general"}`, "/news/general-latest", map[string]string{}},
		{"get-calendar", `{"type":"earnings","from":"2024-01-01"}`, "/earnings-calendar", map[string]string{"from": "2024-01-01"}},
		{"get-economic-indicators", `{"name":"GDP"}`, "/economic-indicators", map[string]string{"name": "GDP"}},
		{"get-analyst-estimates", `{"symbol":"AAPL"}`, "/analyst-estimates", map[string]string{"symbol": "AAPL"}},
		{"get-ratings", `{"type":"grades","symbol":"AAPL"}`, "/grades", map[string]string{"symbol": "AAPL"}},
		{"get-insider-trades", `{"symbol":"AAPL"}`, "/insider-trading/search", map[string]string{"symbol": "AAPL"}},
		{"get-insider-trades", `{"page":2}`, "/insider-trading/latest", map[string]string{"page": "2"}},
		{"get-congressional-trades", `{"chamber":"house"}`, "/house-latest", map[string]string{}},
		{"get-congressional-trades", `{"chamber":"senate","symbol":"AAPL"}`, "/senate-trades", map[string]string{"symbol": "AAPL"}},
		{"get-sec-filings", `{"symbol":"AAPL"}`, "/sec-filings-search/symbol", map[string]string{"symbol": "AAPL"}},
		{"get-sec-filings", `{"formType":"10-K"}`, "/sec-filings-search/form-type", map[string]string{"formType": "10-K"}},
		{"get-etf-holdings", `{"symbol":"VOO"}`, "/etf/holdings", map[string]string{"symbol": "VOO"}},
		{"get-technical-indicator", `{"indicator":"rsi","symbol":"AAPL","periodLength":14}`, "/technical-indicators/rsi", map[string]string{"symbol": "AAPL", "periodLength": "14"}},
	}
	for _, testCase := range cases {
		wire := newWireServer(t, rows)
		output, err := invoke(t, wire, testCase.action, testCase.input)
		require.NoError(t, err, testCase.action)
		requireWire(t, wire, testCase.path, testCase.params)
		require.NotNil(t, output, testCase.action)
	}
}

func TestWireIntegerQueryFormatting(t *testing.T) {
	cases := []struct {
		name   string
		action string
		input  string
		path   string
		param  string
		want   string
	}{
		{
			name: "exponent limit", action: "search-symbols",
			input: `{"query":"apple","limit":1e1}`,
			path:  "/search-symbol", param: "limit", want: "10",
		},
		{
			name: "decimal page", action: "get-news",
			input: `{"type":"stock","page":1.0}`,
			path:  "/news/stock-latest", param: "page", want: "1",
		},
		{
			name: "large technical period", action: "get-technical-indicator",
			input: `{"indicator":"rsi","symbol":"AAPL","periodLength":2288457142539548672053}`,
			path:  "/technical-indicators/rsi", param: "periodLength",
			want: "2288457142539548672053",
		},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			wire := newWireServer(t, `[]`)
			_, err := invoke(t, wire, testCase.action, testCase.input)
			require.NoError(t, err)
			requireWire(t, wire, testCase.path, map[string]string{
				testCase.param: testCase.want,
			})
		})
	}

	t.Run("ordinary numbers and booleans keep their schema semantics", func(t *testing.T) {
		wire := newWireServer(t, `[]`)
		_, err := invoke(t, wire, "search-company-screener",
			`{"priceMoreThan":1.5,"isEtf":true}`)
		require.NoError(t, err)
		requireWire(t, wire, "/company-screener", map[string]string{
			"priceMoreThan": "1.5",
			"isEtf":         "true",
		})
	})

	wire := newWireServer(t, `[]`)
	_, err := invoke(t, wire, "search-symbols", `{"query":"apple","limit":1.5}`)
	require.Error(t, err)
	require.Equal(t, extensionflow.InvocationValidation, classifiedCode(t, err))
	require.Empty(t, wire.requests, "fractional integer must fail before the upstream call")
}

// The normalizing actions return the upstream projections: search results,
// the compact quote, and the historical price rows.
func TestWireNormalizedOutputs(t *testing.T) {
	wire := newWireServer(t, `[{"symbol":"AAPL","name":"Apple Inc.","currency":"USD","stockExchange":"NASDAQ","exchangeShortName":"NASDAQ"}]`)
	output, err := invoke(t, wire, "search-symbols", `{"query":"apple"}`)
	require.NoError(t, err)
	var search struct {
		Results []map[string]any `json:"results"`
	}
	require.NoError(t, json.Unmarshal(output, &search))
	require.Len(t, search.Results, 1)
	require.Equal(t, "Apple Inc.", search.Results[0]["name"])

	wire = newWireServer(t, `[{"symbol":"AAPL","name":"Apple Inc.","price":189.5,"change":1.25,"timestamp":1704451200,"unused":"dropped"}]`)
	output, err = invoke(t, wire, "get-quote", `{"symbol":"AAPL"}`)
	require.NoError(t, err)
	var quoted struct {
		Quote map[string]any `json:"quote"`
	}
	require.NoError(t, json.Unmarshal(output, &quoted))
	require.Equal(t, "AAPL", quoted.Quote["symbol"])
	require.Equal(t, 189.5, quoted.Quote["price"])
	require.Equal(t, float64(1704451200), quoted.Quote["timestamp"])
	require.NotContains(t, quoted.Quote, "unused")

	wire = newWireServer(t, `[{"date":"2024-01-05","open":180.0,"high":181.5,"low":179.0,"close":181.0,"adjClose":181.0,"volume":52000000,"label":"extra"}]`)
	output, err = invoke(t, wire, "get-historical-prices", `{"symbol":"AAPL"}`)
	require.NoError(t, err)
	var historical struct {
		Symbol     string           `json:"symbol"`
		Historical []map[string]any `json:"historical"`
	}
	require.NoError(t, json.Unmarshal(output, &historical))
	require.Equal(t, "AAPL", historical.Symbol)
	require.Len(t, historical.Historical, 1)
	require.Equal(t, "2024-01-05", historical.Historical[0]["date"])
	require.Equal(t, 181.0, historical.Historical[0]["close"])
}

// Failures inside HTTP 200 bodies classify onto the closed set.
func TestWireInBodyErrorClassification(t *testing.T) {
	cases := []struct {
		name     string
		body     string
		expected extensionflow.InvocationErrorCode
	}{
		{"rate limit", `{"message":"You have reached the rate limit"}`, extensionflow.InvocationRateLimited},
		{"limit exceeded", `{"Error Message":"Limit exceeded for today"}`, extensionflow.InvocationRateLimited},
		{"restricted endpoint", `{"message":"Restricted endpoint, please upgrade your plan"}`, extensionflow.InvocationValidation},
		{"credential", `{"error":"Invalid API key"}`, extensionflow.InvocationProviderAuthUnavailable},
		{"unauthorized", `{"message":"Unauthorized access"}`, extensionflow.InvocationProviderAuthUnavailable},
		{"generic message", `{"message":"Unknown symbol"}`, extensionflow.InvocationUpstreamFailure},
	}
	for _, testCase := range cases {
		wire := newWireServer(t, testCase.body)
		_, err := invoke(t, wire, "get-quote", `{"symbol":"AAPL"}`)
		require.Error(t, err, testCase.name)
		require.Equal(t, testCase.expected, classifiedCode(t, err), testCase.name)
	}
}

// HTTP-level failures classify through the status classifier.
func TestWireHTTPStatusClassification(t *testing.T) {
	cases := []struct {
		status   int
		expected extensionflow.InvocationErrorCode
	}{
		{http.StatusUnauthorized, extensionflow.InvocationProviderAuthUnavailable},
		{http.StatusForbidden, extensionflow.InvocationProviderAuthUnavailable},
		{http.StatusTooManyRequests, extensionflow.InvocationRateLimited},
		{http.StatusNotFound, extensionflow.InvocationValidation},
		{http.StatusInternalServerError, extensionflow.InvocationUpstreamFailure},
	}
	for _, testCase := range cases {
		wire := newWireServer(t, `{"message":"failure"}`)
		wire.status = testCase.status
		_, err := invoke(t, wire, "get-quote", `{"symbol":"AAPL"}`)
		require.Error(t, err, "status %d", testCase.status)
		require.Equal(t, testCase.expected, classifiedCode(t, err), "status %d", testCase.status)
	}
}

// Malformed 2xx bodies fail closed: empty responses, invalid JSON, and
// non-array payloads.
func TestWireMalformedBodiesFailClosed(t *testing.T) {
	for _, body := range []string{"", "   ", "not-json", `{"message":"ok"}`} {
		wire := newWireServer(t, body)
		_, err := invoke(t, wire, "get-quote", `{"symbol":"AAPL"}`)
		require.Error(t, err, body)
		require.Equal(t, extensionflow.InvocationUpstreamFailure, classifiedCode(t, err), body)
	}

	t.Run("empty quote array", func(t *testing.T) {
		wire := newWireServer(t, `[]`)
		var invokeErr error
		require.NotPanics(t, func() {
			_, invokeErr = invoke(t, wire, "get-quote", `{"symbol":"AAPL"}`)
		})
		require.Error(t, invokeErr)
		require.Equal(t,
			extensionflow.InvocationUpstreamFailure,
			classifiedCode(t, invokeErr),
		)
	})
}

// Required input validation rejects missing fields before any request.
func TestWireRequiredInputValidation(t *testing.T) {
	wire := newWireServer(t, `[]`)
	for _, testCase := range []struct{ action, input string }{
		{"get-quote", `{}`},
		{"search-symbols", `{"limit":5}`},
		{"list-directory", `{"type":"unknown-type"}`},
		{"get-intraday-prices", `{"symbol":"AAPL","interval":"7min"}`},
		{"get-market-movers", `{"type":"unknown"}`},
	} {
		_, err := invoke(t, wire, testCase.action, testCase.input)
		require.Error(t, err, testCase.action)
		require.Equal(t, extensionflow.InvocationValidation, classifiedCode(t, err), testCase.action)
	}
	require.Empty(t, wire.requests)
}

// The credential probe performs the upstream AAPL quote request.
func TestWireValidateCredentialsProbe(t *testing.T) {
	wire := newWireServer(t, `[{"symbol":"AAPL","price":189.5}]`)
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:          wire.server.URL,
		Auth:             financialmodelingprep.AuthSpec(),
		StatusClassifier: financialmodelingprep.ClassifyStatus,
		Transport:        insecureTransport(),
	})
	require.NoError(t, err)
	require.NoError(t, financialmodelingprep.ValidateCredentialsWithClientForTest(
		context.Background(), client, runner.Credentials{financialmodelingprep.APIKeyField: "key-1"}))
	requireWire(t, wire, "/quote", map[string]string{})
}
