package runtime

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

	"github.com/stretchr/testify/require"
)

type staticConnectionReader struct {
	connection RuntimeConnection
	err        error
}

type recordingConnectionReader struct {
	connection     RuntimeConnection
	err            error
	workspaceID    string
	installationID string
	provider       string
}

func (reader *recordingConnectionReader) ResolveSearchEngineRuntimeConnection(
	_ context.Context,
	workspaceID string,
	installationID string,
	provider string,
) (RuntimeConnection, error) {
	reader.workspaceID = workspaceID
	reader.installationID = installationID
	reader.provider = provider
	return reader.connection, reader.err
}

func (reader staticConnectionReader) ResolveSearchEngineRuntimeConnection(
	context.Context,
	string,
	string,
	string,
) (RuntimeConnection, error) {
	return reader.connection, reader.err
}

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

	tests := []struct {
		name       string
		provider   string
		connection RuntimeConnection
		handler    http.HandlerFunc
		want       []Result
	}{
		{
			name:     "searxng",
			provider: ProviderSearXNG,
			connection: RuntimeConnection{
				Provider: ProviderSearXNG,
				Language: "en",
			},
			handler: func(w http.ResponseWriter, request *http.Request) {
				require.Equal(t, http.MethodGet, request.Method)
				require.Equal(t, "/search", request.URL.Path)
				require.Equal(t, "agentteam extension", request.URL.Query().Get("q"))
				require.Equal(t, "json", request.URL.Query().Get("format"))
				require.Equal(t, "en", request.URL.Query().Get("language"))
				writeJSON(t, w, map[string]any{"results": []any{
					map[string]any{
						"url":     "https://example.com/a#section",
						"title":   "A",
						"content": "alpha",
					},
					map[string]any{
						"url":     "https://example.com/a",
						"title":   "duplicate",
						"content": "duplicate",
					},
					map[string]any{
						"url":     "file:///etc/passwd",
						"title":   "bad",
						"content": "bad",
					},
				}})
			},
			want: []Result{{
				URL:     "https://example.com/a",
				Title:   "A",
				Snippet: "alpha",
			}},
		},
		{
			name:     "brave",
			provider: ProviderBrave,
			connection: RuntimeConnection{
				Provider: ProviderBrave,
				APIKey:   "brave-secret",
			},
			handler: func(w http.ResponseWriter, request *http.Request) {
				require.Equal(t, http.MethodGet, request.Method)
				require.Equal(t, "brave-secret", request.Header.Get("X-Subscription-Token"))
				require.Equal(t, "agentteam extension", request.URL.Query().Get("q"))
				require.Equal(t, "2", request.URL.Query().Get("count"))
				writeJSON(t, w, map[string]any{"web": map[string]any{"results": []any{
					map[string]any{
						"url":         "https://example.com/brave",
						"title":       "Brave",
						"description": "brave snippet",
					},
				}}})
			},
			want: []Result{{
				URL:     "https://example.com/brave",
				Title:   "Brave",
				Snippet: "brave snippet",
			}},
		},
		{
			name:     "tavily",
			provider: ProviderTavily,
			connection: RuntimeConnection{
				Provider: ProviderTavily,
				APIKey:   "tavily-secret",
			},
			handler: func(w http.ResponseWriter, request *http.Request) {
				require.Equal(t, http.MethodPost, request.Method)
				require.Equal(t, "Bearer tavily-secret", request.Header.Get("Authorization"))
				var payload map[string]any
				require.NoError(t, json.NewDecoder(request.Body).Decode(&payload))
				require.Equal(t, "agentteam extension", payload["query"])
				require.EqualValues(t, 2, payload["max_results"])
				writeJSON(t, w, map[string]any{"results": []any{
					map[string]any{
						"url":     "https://example.com/tavily",
						"title":   "Tavily",
						"content": "tavily snippet",
					},
				}})
			},
			want: []Result{{
				URL:     "https://example.com/tavily",
				Title:   "Tavily",
				Snippet: "tavily snippet",
			}},
		},
		{
			name:     "serper",
			provider: ProviderSerper,
			connection: RuntimeConnection{
				Provider: ProviderSerper,
				APIKey:   "serper-secret",
			},
			handler: func(w http.ResponseWriter, request *http.Request) {
				require.Equal(t, http.MethodPost, request.Method)
				require.Equal(t, "serper-secret", request.Header.Get("X-API-KEY"))
				var payload map[string]any
				require.NoError(t, json.NewDecoder(request.Body).Decode(&payload))
				require.Equal(t, "agentteam extension", payload["q"])
				require.EqualValues(t, 2, payload["num"])
				writeJSON(t, w, map[string]any{"organic": []any{
					map[string]any{
						"position": 2,
						"link":     "https://example.com/second",
						"title":    "Second",
						"snippet":  "second snippet",
					},
					map[string]any{
						"position": 1,
						"link":     "https://example.com/first",
						"title":    "First",
						"snippet":  "first snippet",
					},
				}})
			},
			want: []Result{
				{
					URL:     "https://example.com/first",
					Title:   "First",
					Snippet: "first snippet",
				},
				{
					URL:     "https://example.com/second",
					Title:   "Second",
					Snippet: "second snippet",
				},
			},
		},
	}

	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			server := httptest.NewServer(test.handler)
			t.Cleanup(server.Close)

			connection := test.connection
			endpoints := map[string]string{test.provider: server.URL}
			if test.provider == ProviderSearXNG {
				connection.BaseURL = server.URL
				delete(endpoints, test.provider)
			}
			service := newTestService(
				staticConnectionReader{connection: connection},
				endpoints,
			)
			result, err := service.Search(context.Background(), SearchRequest{
				WorkspaceID:           "wsp_1",
				AgentID:               "agt_1",
				AgentMemberID:         "mem_1",
				ServiceInstallationID: "svcinst_1",
				Provider:              test.provider,
				Query:                 " agentteam extension ",
				Limit:                 2,
			})
			require.NoError(t, err)
			require.Equal(t, test.want, result.Results)
		})
	}
}

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

	var gotCount string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		gotCount = request.URL.Query().Get("count")
		writeJSON(t, w, map[string]any{"web": map[string]any{"results": []any{}}})
	}))
	t.Cleanup(server.Close)

	service := newTestService(
		staticConnectionReader{connection: RuntimeConnection{
			Provider: ProviderBrave,
			APIKey:   "secret",
		}},
		map[string]string{ProviderBrave: server.URL},
	)
	result, err := service.Search(context.Background(), SearchRequest{
		WorkspaceID:           "wsp_1",
		AgentID:               "agt_1",
		AgentMemberID:         "mem_1",
		ServiceInstallationID: "svcinst_1",
		Provider:              ProviderBrave,
		Query:                 "query",
	})
	require.NoError(t, err)
	require.Empty(t, result.Results)
	require.Equal(t, "5", gotCount)
}

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

	service := New(staticConnectionReader{})
	_, err := service.Search(context.Background(), SearchRequest{
		WorkspaceID:           "wsp_1",
		AgentID:               "agt_1",
		AgentMemberID:         "mem_1",
		ServiceInstallationID: "svcinst_1",
		Provider:              "unknown",
		Query:                 "query",
	})
	require.Error(t, err)
	require.Equal(t, ErrorValidation, CodeOf(err))
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		http.Error(w, "secret body", http.StatusTooManyRequests)
	}))
	t.Cleanup(server.Close)

	service := newTestService(
		staticConnectionReader{connection: RuntimeConnection{
			Provider: ProviderBrave,
			APIKey:   "secret",
		}},
		map[string]string{ProviderBrave: server.URL},
	)
	_, err := service.Search(context.Background(), SearchRequest{
		WorkspaceID:           "wsp_1",
		AgentID:               "agt_1",
		AgentMemberID:         "mem_1",
		ServiceInstallationID: "svcinst_1",
		Provider:              ProviderBrave,
		Query:                 "query",
	})
	require.Error(t, err)
	require.Equal(t, ErrorRateLimited, CodeOf(err))
	require.NotContains(t, err.Error(), "secret body")
}

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

	service := New(staticConnectionReader{
		connection: RuntimeConnection{Provider: ProviderTavily},
	})
	_, err := service.Search(context.Background(), SearchRequest{
		WorkspaceID:           "wsp_1",
		AgentID:               "agt_1",
		AgentMemberID:         "mem_1",
		ServiceInstallationID: "svcinst_1",
		Provider:              ProviderTavily,
		Query:                 "query",
	})
	require.Error(t, err)
	require.Equal(t, ErrorProviderAuthUnavailable, CodeOf(err))
}

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

	server := httptest.NewServer(http.HandlerFunc(func(
		w http.ResponseWriter,
		_ *http.Request,
	) {
		_, _ = io.WriteString(w, `{"web":{"results":[]}}`)
	}))
	t.Cleanup(server.Close)
	reader := &recordingConnectionReader{connection: RuntimeConnection{
		Provider: ProviderBrave,
		APIKey:   "secret",
	}}
	service := newTestService(
		reader,
		map[string]string{ProviderBrave: server.URL},
	)

	_, err := service.Search(context.Background(), SearchRequest{
		WorkspaceID:           "wsp_exact",
		AgentID:               "agt_1",
		AgentMemberID:         "mem_1",
		ServiceInstallationID: "svcinst_exact",
		Provider:              ProviderBrave,
		Query:                 "query",
	})
	require.NoError(t, err)
	require.Equal(t, "wsp_exact", reader.workspaceID)
	require.Equal(t, "svcinst_exact", reader.installationID)
	require.Equal(t, ProviderBrave, reader.provider)
}

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

	tests := []struct {
		name     string
		err      error
		wantCode ErrorCode
		wantIs   error
	}{
		{
			name: "expected unavailable",
			err: RuntimeConnectionUnavailable(
				errors.New("ambiguous connection"),
			),
			wantCode: ErrorProviderAuthUnavailable,
		},
		{
			name:     "dependency failure",
			err:      errors.New("database offline"),
			wantCode: ErrorUpstreamFailure,
		},
		{
			name:   "context canceled",
			err:    context.Canceled,
			wantIs: context.Canceled,
		},
		{
			name:   "context deadline",
			err:    context.DeadlineExceeded,
			wantIs: context.DeadlineExceeded,
		},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			service := New(&recordingConnectionReader{err: test.err})
			_, err := service.Search(
				context.Background(),
				testSearchRequest(ProviderBrave),
			)
			require.Error(t, err)
			if test.wantIs != nil {
				require.ErrorIs(t, err, test.wantIs)
				require.Empty(t, CodeOf(err))
				return
			}
			require.Equal(t, test.wantCode, CodeOf(err))
			require.NotContains(t, err.Error(), test.err.Error())
		})
	}
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		require.Equal(t, http.MethodGet, request.Method)
		require.Equal(t, "AgentTeam", request.URL.Query().Get("q"))
		require.Equal(t, "1", request.URL.Query().Get("count"))
		require.Equal(t, "server-only-secret", request.Header.Get("X-Subscription-Token"))
		_, _ = io.WriteString(w, `{"web":{"results":[]}}`)
	}))
	t.Cleanup(server.Close)

	service := newTestService(nil, map[string]string{ProviderBrave: server.URL})
	require.NoError(t, service.TestConnection(context.Background(), RuntimeConnection{
		Provider: ProviderBrave,
		APIKey:   "server-only-secret",
	}))
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = io.WriteString(w, `{"web":{"results":[{"url":"https://example.com","description":"`)
		_, _ = io.WriteString(w, strings.Repeat("x", MaxRawResponseBytes+1))
		_, _ = io.WriteString(w, `"}]}}`)
	}))
	t.Cleanup(server.Close)

	service := newTestService(
		staticConnectionReader{connection: RuntimeConnection{
			Provider: ProviderBrave,
			APIKey:   "secret",
		}},
		map[string]string{ProviderBrave: server.URL},
	)
	_, err := service.Search(context.Background(), SearchRequest{
		WorkspaceID:           "wsp_1",
		AgentID:               "agt_1",
		AgentMemberID:         "mem_1",
		ServiceInstallationID: "svcinst_1",
		Provider:              ProviderBrave,
		Query:                 "query",
	})
	require.Error(t, err)
	require.Equal(t, ErrorUpstreamFailure, CodeOf(err))
}

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

	results := normalizeResults([]Result{
		{
			URL:     "https://example.com/ok#fragment",
			Title:   strings.Repeat("题", maxTitleRunes+10),
			Snippet: strings.Repeat("文", maxSnippetRunes+10),
		},
		{URL: "javascript:alert(1)", Title: "bad"},
		{URL: "https://example.com/ok", Title: "duplicate"},
		{URL: "https://example.com/next", Title: "next"},
	}, 1)

	require.Len(t, results, 1)
	require.Equal(t, "https://example.com/ok", results[0].URL)
	require.Len(t, []rune(results[0].Title), maxTitleRunes)
	require.Len(t, []rune(results[0].Snippet), maxSnippetRunes)
}

func writeJSON(t *testing.T, w http.ResponseWriter, value any) {
	t.Helper()
	w.Header().Set("Content-Type", "application/json")
	require.NoError(t, json.NewEncoder(w).Encode(value))
}

func newTestService(
	connections ConnectionReader,
	endpoints map[string]string,
) *Service {
	service := New(connections)
	for provider, endpoint := range endpoints {
		service.endpoints[provider] = endpoint
	}
	return service
}
