// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.

package mcp

import (
	"context"
	"fmt"
	"log/slog"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/google/jsonschema-go/jsonschema"
)

type Item struct {
	Name  string
	Value string
}

type ListTestParams struct {
	Cursor string
}

func (p *ListTestParams) cursorPtr() *string {
	return &p.Cursor
}

type ListTestResult struct {
	Items      []*Item
	NextCursor string
}

func (r *ListTestResult) nextCursorPtr() *string {
	return &r.NextCursor
}

var allItems = []*Item{
	{"alpha", "val-A"},
	{"bravo", "val-B"},
	{"charlie", "val-C"},
	{"delta", "val-D"},
	{"echo", "val-E"},
	{"foxtrot", "val-F"},
	{"golf", "val-G"},
	{"hotel", "val-H"},
	{"india", "val-I"},
	{"juliet", "val-J"},
	{"kilo", "val-K"},
}

// generatePaginatedResults is a helper to create a sequence of mock responses for pagination.
// It simulates a server returning items in pages based on a given page size.
func generatePaginatedResults(all []*Item, pageSize int) []*ListTestResult {
	if len(all) == 0 {
		return []*ListTestResult{{Items: []*Item{}, NextCursor: ""}}
	}
	if pageSize <= 0 {
		panic("pageSize must be greater than 0")
	}
	numPages := (len(all) + pageSize - 1) / pageSize // Ceiling division
	var results []*ListTestResult
	for i := range numPages {
		startIndex := i * pageSize
		endIndex := min(startIndex+pageSize, len(all)) // Use min to prevent out of bounds
		nextCursor := ""
		if endIndex < len(all) { // If there are more items after this page
			nextCursor = fmt.Sprintf("cursor_%d", endIndex)
		}
		results = append(results, &ListTestResult{Items: all[startIndex:endIndex], NextCursor: nextCursor})
	}
	return results
}

func TestClientPaginateBasic(t *testing.T) {
	ctx := context.Background()
	testCases := []struct {
		name          string
		results       []*ListTestResult
		mockError     error
		initialParams *ListTestParams
		expected      []*Item
		expectError   bool
	}{
		{
			name:     "SinglePageAllItems",
			results:  generatePaginatedResults(allItems, len(allItems)),
			expected: allItems,
		},
		{
			name:     "MultiplePages",
			results:  generatePaginatedResults(allItems, 3),
			expected: allItems,
		},
		{
			name:     "EmptyResults",
			results:  generatePaginatedResults([]*Item{}, 10),
			expected: nil,
		},
		{
			name:        "ListFuncReturnsErrorImmediately",
			results:     []*ListTestResult{{}},
			mockError:   fmt.Errorf("API error on first call"),
			expected:    nil,
			expectError: true,
		},
		{
			name:          "InitialCursorProvided",
			initialParams: &ListTestParams{Cursor: "cursor_2"},
			results:       generatePaginatedResults(allItems[2:], 3),
			expected:      allItems[2:],
		},
		{
			name:          "CursorBeyondAllItems",
			initialParams: &ListTestParams{Cursor: "cursor_999"},
			results:       []*ListTestResult{{Items: []*Item{}, NextCursor: ""}},
			expected:      nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			listFunc := func(ctx context.Context, params *ListTestParams) (*ListTestResult, error) {
				if len(tc.results) == 0 {
					t.Fatalf("listFunc called but no more results defined for test case %q", tc.name)
				}
				res := tc.results[0]
				tc.results = tc.results[1:]
				var err error
				if tc.mockError != nil {
					err = tc.mockError
				}
				return res, err
			}

			params := tc.initialParams
			if tc.initialParams == nil {
				params = &ListTestParams{}
			}

			var gotItems []*Item
			var iterationErr error
			seq := paginate(ctx, params, listFunc, func(r *ListTestResult) []*Item { return r.Items })
			for item, err := range seq {
				if err != nil {
					iterationErr = err
					break
				}
				gotItems = append(gotItems, item)
			}
			if tc.expectError {
				if iterationErr == nil {
					t.Errorf("paginate() expected an error during iteration, but got none")
				}
			} else {
				if iterationErr != nil {
					t.Errorf("paginate() got: %v, want: nil", iterationErr)
				}
			}
			if diff := cmp.Diff(tc.expected, gotItems, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
				t.Fatalf("paginate() mismatch (-want +got):\n%s", diff)
			}
		})
	}
}

func TestClientLogger(t *testing.T) {
	// Case 1: No logger provided
	c1 := NewClient(&Implementation{Name: "test", Version: "1.0"}, nil)
	if c1.opts.Logger == nil {
		t.Error("expected default logger, got nil")
	}

	// Case 2: Logger provided
	logger := slog.Default()
	c2 := NewClient(&Implementation{Name: "test", Version: "1.0"}, &ClientOptions{
		Logger: logger,
	})
	if c2.opts.Logger != logger {
		t.Error("expected provided logger, got different one")
	}
}

func TestClientPaginateVariousPageSizes(t *testing.T) {
	ctx := context.Background()
	for i := 1; i < len(allItems)+1; i++ {
		testname := fmt.Sprintf("PageSize=%d", i)
		t.Run(testname, func(t *testing.T) {
			results := generatePaginatedResults(allItems, i)
			listFunc := func(ctx context.Context, params *ListTestParams) (*ListTestResult, error) {
				res := results[0]
				results = results[1:]
				return res, nil
			}
			var gotItems []*Item
			seq := paginate(ctx, &ListTestParams{}, listFunc, func(r *ListTestResult) []*Item { return r.Items })
			for item, err := range seq {
				if err != nil {
					t.Fatalf("paginate() unexpected error during iteration: %v", err)
				}
				gotItems = append(gotItems, item)
			}
			if diff := cmp.Diff(allItems, gotItems, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
				t.Fatalf("paginate() mismatch (-want +got):\n%s", diff)
			}
		})
	}
}

func TestClientCapabilities(t *testing.T) {
	testCases := []struct {
		name             string
		configureClient  func(s *Client)
		clientOpts       ClientOptions
		protocolVersion  string // defaults to latestProtocolVersion if empty
		wantCapabilities *ClientCapabilities
	}{
		{
			name:            "default",
			configureClient: func(s *Client) {},
			wantCapabilities: &ClientCapabilities{
				Roots:   RootCapabilities{ListChanged: true},
				RootsV2: &RootCapabilities{ListChanged: true},
			},
		},
		{
			name:            "with sampling",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				CreateMessageHandler: func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) {
					return nil, nil
				},
			},
			wantCapabilities: &ClientCapabilities{
				Roots:    RootCapabilities{ListChanged: true},
				RootsV2:  &RootCapabilities{ListChanged: true},
				Sampling: &SamplingCapabilities{},
			},
		},
		{
			name:            "with elicitation",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
					return nil, nil
				},
			},
			protocolVersion: protocolVersion20251125,
			wantCapabilities: &ClientCapabilities{
				Roots:   RootCapabilities{ListChanged: true},
				RootsV2: &RootCapabilities{ListChanged: true},
				Elicitation: &ElicitationCapabilities{
					Form: &FormElicitationCapabilities{},
				},
			},
		},
		{
			name:            "with elicitation (old protocol)",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
					return nil, nil
				},
			},
			protocolVersion: protocolVersion20250618,
			wantCapabilities: &ClientCapabilities{
				Roots:       RootCapabilities{ListChanged: true},
				RootsV2:     &RootCapabilities{ListChanged: true},
				Elicitation: &ElicitationCapabilities{},
			},
		},
		{
			name:            "with URL elicitation",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				Capabilities: &ClientCapabilities{
					Roots:   RootCapabilities{ListChanged: true},
					RootsV2: &RootCapabilities{ListChanged: true},
					Elicitation: &ElicitationCapabilities{
						URL: &URLElicitationCapabilities{},
					},
				},
				ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
					return nil, nil
				},
			},
			wantCapabilities: &ClientCapabilities{
				Roots:   RootCapabilities{ListChanged: true},
				RootsV2: &RootCapabilities{ListChanged: true},
				Elicitation: &ElicitationCapabilities{
					URL: &URLElicitationCapabilities{},
				},
			},
		},
		{
			name:            "with form and URL elicitation",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				Capabilities: &ClientCapabilities{
					Roots:   RootCapabilities{ListChanged: true},
					RootsV2: &RootCapabilities{ListChanged: true},
					Elicitation: &ElicitationCapabilities{
						Form: &FormElicitationCapabilities{},
						URL:  &URLElicitationCapabilities{},
					},
				},
				ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
					return nil, nil
				},
			},
			wantCapabilities: &ClientCapabilities{
				Roots:   RootCapabilities{ListChanged: true},
				RootsV2: &RootCapabilities{ListChanged: true},
				Elicitation: &ElicitationCapabilities{
					Form: &FormElicitationCapabilities{},
					URL:  &URLElicitationCapabilities{},
				},
			},
		},
		{
			name:            "no capabilities",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				Capabilities: &ClientCapabilities{},
			},
			wantCapabilities: &ClientCapabilities{},
		},
		{
			name:            "no roots",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				Capabilities: &ClientCapabilities{
					Sampling: &SamplingCapabilities{},
				},
			},
			wantCapabilities: &ClientCapabilities{
				Sampling: &SamplingCapabilities{},
			},
		},
		{
			name:            "roots-no list",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				Capabilities: &ClientCapabilities{
					RootsV2: &RootCapabilities{ListChanged: false},
				},
			},
			wantCapabilities: &ClientCapabilities{
				RootsV2: &RootCapabilities{ListChanged: false},
			},
		},
		{
			name:            "custom capabilities with sampling",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				Capabilities: &ClientCapabilities{
					RootsV2: &RootCapabilities{ListChanged: true},
				},
				CreateMessageHandler: func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) {
					return nil, nil
				},
			},
			wantCapabilities: &ClientCapabilities{
				Roots:    RootCapabilities{ListChanged: true},
				RootsV2:  &RootCapabilities{ListChanged: true},
				Sampling: &SamplingCapabilities{},
			},
		},
		{
			name:            "elicitation override",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				Capabilities: &ClientCapabilities{
					Elicitation: &ElicitationCapabilities{
						URL: &URLElicitationCapabilities{},
					},
				},
				ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
					return nil, nil
				},
			},
			wantCapabilities: &ClientCapabilities{
				Elicitation: &ElicitationCapabilities{
					URL: &URLElicitationCapabilities{},
				},
			},
		},
		{
			name:            "custom capabilities with experimental",
			configureClient: func(s *Client) {},
			clientOpts: ClientOptions{
				Capabilities: &ClientCapabilities{
					Experimental: map[string]any{"custom": "value"},
					RootsV2:      &RootCapabilities{ListChanged: true},
				},
			},
			wantCapabilities: &ClientCapabilities{
				Experimental: map[string]any{"custom": "value"},
				Roots:        RootCapabilities{ListChanged: true},
				RootsV2:      &RootCapabilities{ListChanged: true},
			},
		},
		{
			name:            "extensions preserved",
			configureClient: func(s *Client) {},
			clientOpts: func() ClientOptions {
				caps := &ClientCapabilities{
					RootsV2: &RootCapabilities{ListChanged: true},
				}
				caps.AddExtension("io.example/ext1", map[string]any{"key": "value"})
				caps.AddExtension("io.example/ext2", nil)
				return ClientOptions{Capabilities: caps}
			}(),
			wantCapabilities: &ClientCapabilities{
				Extensions: map[string]any{
					"io.example/ext1": map[string]any{"key": "value"},
					"io.example/ext2": map[string]any{},
				},
				Roots:   RootCapabilities{ListChanged: true},
				RootsV2: &RootCapabilities{ListChanged: true},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			client := NewClient(testImpl, &tc.clientOpts)
			tc.configureClient(client)
			protocolVersion := tc.protocolVersion
			if protocolVersion == "" {
				protocolVersion = latestProtocolVersion
			}
			gotCapabilities := client.capabilities(protocolVersion)
			if diff := cmp.Diff(tc.wantCapabilities, gotCapabilities); diff != "" {
				t.Errorf("capabilities() mismatch (-want +got):\n%s", diff)
			}
		})
	}
}

func TestClientCapabilitiesOverWire(t *testing.T) {
	testCases := []struct {
		name             string
		clientOpts       *ClientOptions
		wantCapabilities *ClientCapabilities
	}{
		{
			name:       "Default capabilities",
			clientOpts: nil,
			wantCapabilities: &ClientCapabilities{
				Roots:   RootCapabilities{ListChanged: true},
				RootsV2: &RootCapabilities{ListChanged: true},
			},
		},
		{
			name: "Custom Capabilities with roots listChanged false",
			clientOpts: &ClientOptions{
				Capabilities: &ClientCapabilities{
					RootsV2: &RootCapabilities{ListChanged: false},
				},
			},
			wantCapabilities: &ClientCapabilities{
				Roots:   RootCapabilities{ListChanged: false},
				RootsV2: &RootCapabilities{ListChanged: false},
			},
		},
		{
			name: "Dynamic sampling capability",
			clientOpts: &ClientOptions{
				Capabilities: &ClientCapabilities{
					RootsV2: &RootCapabilities{ListChanged: true},
				},
				CreateMessageHandler: func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) {
					return nil, nil
				},
			},
			wantCapabilities: &ClientCapabilities{
				Roots:    RootCapabilities{ListChanged: true},
				RootsV2:  &RootCapabilities{ListChanged: true},
				Sampling: &SamplingCapabilities{},
			},
		},
		{
			name: "Empty capabilities disables defaults",
			clientOpts: &ClientOptions{
				Capabilities: &ClientCapabilities{},
			},
			wantCapabilities: &ClientCapabilities{},
		},
		{
			name: "Extensions over wire",
			clientOpts: func() *ClientOptions {
				caps := &ClientCapabilities{
					RootsV2: &RootCapabilities{ListChanged: true},
				}
				caps.AddExtension("io.example/ext", map[string]any{"key": "value"})
				return &ClientOptions{Capabilities: caps}
			}(),
			wantCapabilities: &ClientCapabilities{
				Extensions: map[string]any{
					"io.example/ext": map[string]any{"key": "value"},
				},
				Roots:   RootCapabilities{ListChanged: true},
				RootsV2: &RootCapabilities{ListChanged: true},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			ctx := context.Background()

			// Create client.
			impl := &Implementation{Name: "testClient", Version: "v1.0.0"}
			client := NewClient(impl, tc.clientOpts)

			// Connect client and server.
			cTransport, sTransport := NewInMemoryTransports()
			server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
			ss, err := server.Connect(ctx, sTransport, nil)
			if err != nil {
				t.Fatal(err)
			}
			defer ss.Close()

			cs, err := client.Connect(ctx, cTransport, nil)
			if err != nil {
				t.Fatal(err)
			}
			defer cs.Close()

			// Check that the server received the expected capabilities.
			initParams := ss.InitializeParams()
			if initParams == nil {
				t.Fatal("InitializeParams is nil")
			}

			if diff := cmp.Diff(tc.wantCapabilities, initParams.Capabilities); diff != "" {
				t.Errorf("Capabilities mismatch (-want +got):\n%s", diff)
			}
		})
	}
}
