//go:build e2e

package e2e_test

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"os/exec"
	"slices"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/github/github-mcp-server/internal/ghmcp"
	"github.com/github/github-mcp-server/pkg/github"
	"github.com/github/github-mcp-server/pkg/translations"
	gogithub "github.com/google/go-github/v89/github"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/stretchr/testify/require"
)

var (
	// Shared variables and sync.Once instances to ensure one-time execution
	getTokenOnce sync.Once
	token        string

	getHostOnce sync.Once
	host        string

	buildOnce  sync.Once
	buildError error

	// Rate limit management
	rateLimitMu sync.Mutex
)

// minRateLimitRemaining is the minimum number of API requests we want to have
// remaining before we start waiting for the rate limit to reset.
const minRateLimitRemaining = 50

// getE2EToken ensures the environment variable is checked only once and returns the token
func getE2EToken(t *testing.T) string {
	getTokenOnce.Do(func() {
		token = os.Getenv("GITHUB_MCP_SERVER_E2E_TOKEN")
		if token == "" {
			t.Fatalf("GITHUB_MCP_SERVER_E2E_TOKEN environment variable is not set")
		}
	})
	return token
}

// getE2EHost ensures the environment variable is checked only once and returns the host
func getE2EHost() string {
	getHostOnce.Do(func() {
		host = os.Getenv("GITHUB_MCP_SERVER_E2E_HOST")
	})
	return host
}

func getRESTClient(t *testing.T) *gogithub.Client {
	// Get token and ensure Docker image is built
	token := getE2EToken(t)

	// Create a new GitHub client with the token
	ghClient := gogithub.NewClient(nil).WithAuthToken(token)

	if host := getE2EHost(); host != "" && host != "https://github.com" {
		var err error
		// Currently this works for GHEC because the API is exposed at the api subdomain and the path prefix
		// but it would be preferable to extract the host parsing from the main server logic, and use it here.
		ghClient, err = ghClient.WithEnterpriseURLs(host, host)
		require.NoError(t, err, "expected to create GitHub client with host")
	}

	return ghClient
}

// waitForRateLimit checks the current rate limit and waits if necessary.
// It ensures we have at least minRateLimitRemaining requests available before proceeding.
func waitForRateLimit(t *testing.T) {
	rateLimitMu.Lock()
	defer rateLimitMu.Unlock()

	ghClient := getRESTClient(t)
	ctx := context.Background()

	rateLimits, _, err := ghClient.RateLimit.Get(ctx)
	if err != nil {
		t.Logf("Warning: failed to check rate limit: %v", err)
		return
	}

	core := rateLimits.Core
	if core.Remaining < minRateLimitRemaining {
		waitDuration := time.Until(core.Reset.Time) + time.Second // Add 1 second buffer
		if waitDuration > 0 {
			t.Logf("Rate limit low (%d/%d remaining). Waiting %v until reset...",
				core.Remaining, core.Limit, waitDuration.Round(time.Second))
			time.Sleep(waitDuration)
			t.Log("Rate limit reset, continuing...")
		}
	} else {
		t.Logf("Rate limit OK: %d/%d remaining (reset in %v)",
			core.Remaining, core.Limit, time.Until(core.Reset.Time).Round(time.Second))
	}
}

// ensureDockerImageBuilt makes sure the Docker image is built only once across all tests
func ensureDockerImageBuilt(t *testing.T) {
	buildOnce.Do(func() {
		t.Log("Building Docker image for e2e tests...")
		cmd := exec.Command("docker", "build", "-t", "github/e2e-github-mcp-server", ".")
		cmd.Dir = ".." // Run this in the context of the root, where the Dockerfile is located.
		output, err := cmd.CombinedOutput()
		buildError = err
		if err != nil {
			t.Logf("Docker build output: %s", string(output))
		}
	})

	// Check if the build was successful
	require.NoError(t, buildError, "expected to build Docker image successfully")
}

// clientOpts holds configuration options for the MCP client setup
type clientOpts struct {
	// Toolsets to enable in the MCP server
	enabledToolsets []string
}

// clientOption defines a function type for configuring ClientOpts
type clientOption func(*clientOpts)

// withToolsets returns an option that either sets the GITHUB_TOOLSETS envvar when executing in docker,
// or sets the toolsets in the MCP server when running in-process.
func withToolsets(toolsets []string) clientOption {
	return func(opts *clientOpts) {
		opts.enabledToolsets = toolsets
	}
}

func setupMCPClient(t *testing.T, options ...clientOption) *mcp.ClientSession {
	// Check rate limit before setting up the client
	waitForRateLimit(t)

	// Get token and ensure Docker image is built
	token := getE2EToken(t)

	// Create and configure options with default to all toolsets
	opts := &clientOpts{
		enabledToolsets: []string{"all"},
	}

	// Apply all options to configure the opts struct
	for _, option := range options {
		option(opts)
	}

	ctx := context.Background()

	// By default, we run the tests including the Docker image, but with DEBUG
	// enabled, we run the server in-process, allowing for easier debugging.
	var session *mcp.ClientSession
	if os.Getenv("GITHUB_MCP_SERVER_E2E_DEBUG") == "" {
		ensureDockerImageBuilt(t)

		// Prepare Docker arguments
		args := []string{
			"run",
			"-i",
			"--rm",
			"-e",
			"GITHUB_PERSONAL_ACCESS_TOKEN", // Personal access token is all required
		}

		host := getE2EHost()
		if host != "" {
			args = append(args, "-e", "GITHUB_HOST")
		}

		// Add toolsets environment variable to the Docker arguments
		if len(opts.enabledToolsets) > 0 {
			args = append(args, "-e", "GITHUB_TOOLSETS")
		}

		// Add the image name
		args = append(args, "github/e2e-github-mcp-server")

		// Construct the env vars for the MCP Client to execute docker with
		// We need to include os.Environ() so docker can find its socket and config
		dockerEnvVars := append(os.Environ(),
			fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", token),
			fmt.Sprintf("GITHUB_TOOLSETS=%s", strings.Join(opts.enabledToolsets, ",")),
		)

		if host != "" {
			dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("GITHUB_HOST=%s", host))
		}

		// Create the client using CommandTransport
		t.Log("Starting Stdio MCP client...")
		transport := &mcp.CommandTransport{Command: exec.Command("docker", args...)}
		transport.Command.Env = dockerEnvVars
		client := mcp.NewClient(&mcp.Implementation{
			Name:    "e2e-test-client",
			Version: "0.0.1",
		}, nil)
		var err error
		session, err = client.Connect(ctx, transport, nil)
		require.NoError(t, err, "expected to connect client successfully")
	} else {
		// We need this because the fully compiled server has a default for the viper config, which is
		// not in scope for using the MCP server directly. This probably indicates that we should refactor
		// so that there is a shared setup mechanism, but let's wait till we feel more friction.
		enabledToolsets := opts.enabledToolsets
		if enabledToolsets == nil {
			enabledToolsets = github.GetDefaultToolsetIDs()
		}

		ghServer, err := ghmcp.NewMCPServer(ghmcp.MCPServerConfig{
			Token:           token,
			EnabledToolsets: enabledToolsets,
			Host:            getE2EHost(),
			Translator:      translations.NullTranslationHelper,
		})
		require.NoError(t, err, "expected to construct MCP server successfully")

		t.Log("Starting In Process MCP client...")
		serverTransport, clientTransport := mcp.NewInMemoryTransports()
		go func() {
			_ = ghServer.Run(ctx, serverTransport)
		}()
		client := mcp.NewClient(&mcp.Implementation{
			Name:    "e2e-test-client",
			Version: "0.0.1",
		}, nil)
		session, err = client.Connect(ctx, clientTransport, nil)
		require.NoError(t, err, "expected to create in-process client successfully")
	}

	t.Cleanup(func() {
		require.NoError(t, session.Close(), "expected to close client successfully")
	})

	return session
}

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

	mcpClient := setupMCPClient(t)
	ctx := context.Background()

	// When we call the "get_me" tool
	response, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")

	require.False(t, response.IsError, fmt.Sprintf("expected result not to be an error: %+v", response))
	require.Len(t, response.Content, 1, "expected content to have one item")

	textContent, ok := response.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedContent struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedContent)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	// Then the login in the response should match the login obtained via the same
	// token using the GitHub API.
	ghClient := getRESTClient(t)
	user, _, err := ghClient.Users.Get(context.Background(), "")
	require.NoError(t, err, "expected to get user successfully")
	require.Equal(t, trimmedContent.Login, *user.Login, "expected login to match")

}

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

	mcpClient := setupMCPClient(
		t,
		withToolsets([]string{"repos", "issues"}),
	)

	ctx := context.Background()

	response, err := mcpClient.ListTools(ctx, &mcp.ListToolsParams{})
	require.NoError(t, err, "expected to list tools successfully")

	// We could enumerate the tools here, but we'll need to expose that information
	// declaratively in the MCP server, so for the moment let's just check the existence
	// of an issue and repo tool, and the non-existence of a pull_request tool.
	var toolsContains = func(expectedName string) bool {
		return slices.ContainsFunc(response.Tools, func(tool *mcp.Tool) bool {
			return tool.Name == expectedName
		})
	}

	require.True(t, toolsContains("issue_read"), "expected to find 'issue_read' tool")
	require.True(t, toolsContains("list_branches"), "expected to find 'list_branches' tool")
	require.False(t, toolsContains("pull_request_read"), "expected not to find 'pull_request_read' tool")
}

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

	mcpClient := setupMCPClient(t)

	ctx := context.Background()

	// First, who am I

	t.Log("Getting current user...")
	resp, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok := resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetMeText struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	currentOwner := trimmedGetMeText.Login

	// Then create a repository with a README (via autoInit)
	repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())

	t.Logf("Creating repository %s/%s...", currentOwner, repoName)
	_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_repository",
		Arguments: map[string]any{
			"name":     repoName,
			"private":  true,
			"autoInit": true,
		},
	})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Cleanup the repository after the test
	t.Cleanup(func() {
		// MCP Server doesn't support deletions, but we can use the GitHub Client
		ghClient := getRESTClient(t)
		t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
		_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
		require.NoError(t, err, "expected to delete repository successfully")
	})

	// Then create a tag
	// MCP Server doesn't support tag creation, but we can use the GitHub Client
	ghClient := getRESTClient(t)
	t.Logf("Creating tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
	ref, _, err := ghClient.Git.GetRef(context.Background(), currentOwner, repoName, "refs/heads/main")
	require.NoError(t, err, "expected to get ref successfully")

	tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, gogithub.CreateTag{
		Tag:     "v0.0.1",
		Message: "v0.0.1",
		Object:  *ref.Object.SHA,
		Type:    "commit",
	})
	require.NoError(t, err, "expected to create tag object successfully")

	_, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, gogithub.CreateRef{
		Ref: "refs/tags/v0.0.1",
		SHA: *tagObj.SHA,
	})
	require.NoError(t, err, "expected to create tag ref successfully")

	// List the tags

	t.Logf("Listing tags for %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "list_tags",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
		},
	})
	require.NoError(t, err, "expected to call 'list_tags' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedTags []struct {
		Name   string `json:"name"`
		Commit struct {
			SHA string `json:"sha"`
		} `json:"commit"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedTags)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	require.Len(t, trimmedTags, 1, "expected to find one tag")
	require.Equal(t, "v0.0.1", trimmedTags[0].Name, "expected tag name to match")
	require.Equal(t, *ref.Object.SHA, trimmedTags[0].Commit.SHA, "expected tag SHA to match")

	// And fetch an individual tag

	t.Logf("Getting tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "get_tag",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
			"tag":   "v0.0.1",
		},
	})
	require.NoError(t, err, "expected to call 'get_tag' tool successfully")
	require.False(t, resp.IsError, "expected result not to be an error")

	var trimmedTag []struct { // don't understand why this is an array
		Name   string `json:"name"`
		Commit struct {
			SHA string `json:"sha"`
		} `json:"commit"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedTag)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	require.Len(t, trimmedTag, 1, "expected to find one tag")
	require.Equal(t, "v0.0.1", trimmedTag[0].Name, "expected tag name to match")
	require.Equal(t, *ref.Object.SHA, trimmedTag[0].Commit.SHA, "expected tag SHA to match")
}

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

	mcpClient := setupMCPClient(t)

	ctx := context.Background()

	// First, who am I

	t.Log("Getting current user...")
	resp, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok := resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetMeText struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	currentOwner := trimmedGetMeText.Login

	// Then create a repository with a README (via autoInit)
	repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
	t.Logf("Creating repository %s/%s...", currentOwner, repoName)
	_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_repository",
		Arguments: map[string]any{
			"name":     repoName,
			"private":  true,
			"autoInit": true,
		},
	})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Cleanup the repository after the test
	t.Cleanup(func() {
		// MCP Server doesn't support deletions, but we can use the GitHub Client
		ghClient := getRESTClient(t)
		t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
		_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
		require.NoError(t, err, "expected to delete repository successfully")
	})

	// Create a branch on which to create a new commit

	t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_branch",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"branch":      "test-branch",
			"from_branch": "main",
		},
	})
	require.NoError(t, err, "expected to call 'create_branch' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a commit with a new file

	t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_or_update_file",
		Arguments: map[string]any{
			"owner":   currentOwner,
			"repo":    repoName,
			"path":    "test-file.txt",
			"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
			"message": "Add test file",
			"branch":  "test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Check the file exists

	t.Logf("Getting file contents in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "get_file_contents",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
			"path":  "test-file.txt",
			"ref":   "refs/heads/test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'get_file_contents' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	embeddedResource, ok := resp.Content[1].(*mcp.EmbeddedResource)
	require.True(t, ok, "expected content to be of type EmbeddedResource")

	// Access Resource directly - ResourceContents is a pointer, not an interface
	textResource := embeddedResource.Resource
	require.NotNil(t, textResource, "expected embedded resource to have Resource")

	require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), textResource.Text, "expected file content to match")

	// Delete the file

	t.Logf("Deleting file in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "delete_file",
		Arguments: map[string]any{
			"owner":   currentOwner,
			"repo":    repoName,
			"path":    "test-file.txt",
			"message": "Delete test file",
			"branch":  "test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'delete_file' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// See that there is a commit that removes the file

	t.Logf("Listing commits in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "list_commits",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
			"sha":   "test-branch", // can be SHA or branch, which is an unfortunate API design
		},
	})
	require.NoError(t, err, "expected to call 'list_commits' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedListCommitsText []struct {
		SHA    string `json:"sha"`
		Commit struct {
			Message string `json:"message"`
		}
		Files []struct {
			Filename  string `json:"filename"`
			Deletions int    `json:"deletions"`
		}
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedListCommitsText)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	require.GreaterOrEqual(t, len(trimmedListCommitsText), 1, "expected to find at least one commit")

	deletionCommit := trimmedListCommitsText[0]
	require.Equal(t, "Delete test file", deletionCommit.Commit.Message, "expected commit message to match")

	// Now get the commit so we can look at the file changes because list_commits doesn't include them

	t.Logf("Getting commit %s/%s:%s...", currentOwner, repoName, deletionCommit.SHA)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "get_commit",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
			"sha":   deletionCommit.SHA,
		},
	})
	require.NoError(t, err, "expected to call 'get_commit' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetCommitText struct {
		Files []struct {
			Filename  string `json:"filename"`
			Deletions int    `json:"deletions"`
		}
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetCommitText)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	require.Len(t, trimmedGetCommitText.Files, 1, "expected to find one file change")
	require.Equal(t, "test-file.txt", trimmedGetCommitText.Files[0].Filename, "expected filename to match")
	require.Equal(t, 1, trimmedGetCommitText.Files[0].Deletions, "expected one deletion")
}

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

	mcpClient := setupMCPClient(t)

	ctx := context.Background()

	// First, who am I

	t.Log("Getting current user...")
	resp, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok := resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetMeText struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	currentOwner := trimmedGetMeText.Login

	// Then create a repository with a README (via autoInit)
	repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
	t.Logf("Creating repository %s/%s...", currentOwner, repoName)
	_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_repository",
		Arguments: map[string]any{
			"name":     repoName,
			"private":  true,
			"autoInit": true,
		},
	})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Cleanup the repository after the test
	t.Cleanup(func() {
		// MCP Server doesn't support deletions, but we can use the GitHub Client
		ghClient := getRESTClient(t)
		t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
		_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
		require.NoError(t, err, "expected to delete repository successfully")
	})

	// Create a branch on which to create a new commit

	t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_branch",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"branch":      "test-branch",
			"from_branch": "main",
		},
	})
	require.NoError(t, err, "expected to call 'create_branch' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a commit with a new file

	t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_or_update_file",
		Arguments: map[string]any{
			"owner":   currentOwner,
			"repo":    repoName,
			"path":    "test-dir/test-file.txt",
			"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
			"message": "Add test file",
			"branch":  "test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	_, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	// Check the file exists

	t.Logf("Getting file contents in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "get_file_contents",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
			"path":  "test-dir/test-file.txt",
			"ref":   "refs/heads/test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'get_file_contents' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	embeddedResource, ok := resp.Content[1].(*mcp.EmbeddedResource)
	require.True(t, ok, "expected content to be of type EmbeddedResource")

	// Access Resource directly - ResourceContents is a pointer, not an interface
	textResource := embeddedResource.Resource
	require.NotNil(t, textResource, "expected embedded resource to have Resource")

	require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), textResource.Text, "expected file content to match")

	// Delete the directory containing the file

	t.Logf("Deleting directory in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "delete_file",
		Arguments: map[string]any{
			"owner":   currentOwner,
			"repo":    repoName,
			"path":    "test-dir/test-file.txt",
			"message": "Delete test directory",
			"branch":  "test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'delete_file' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// See that there is a commit that removes the directory

	t.Logf("Listing commits in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "list_commits",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
			"sha":   "test-branch", // can be SHA or branch, which is an unfortunate API design
		},
	})
	require.NoError(t, err, "expected to call 'list_commits' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedListCommitsText []struct {
		SHA    string `json:"sha"`
		Commit struct {
			Message string `json:"message"`
		}
		Files []struct {
			Filename  string `json:"filename"`
			Deletions int    `json:"deletions"`
		} `json:"files"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedListCommitsText)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	require.GreaterOrEqual(t, len(trimmedListCommitsText), 1, "expected to find at least one commit")

	// Find the deletion commit (list_commits returns in reverse chronological order,
	// but timing can sometimes cause unexpected ordering)
	// TODO: The delete_file tool only deletes individual files, not directories.
	// This test creates a file in test-dir/ and deletes it, but doesn't actually
	// test recursive directory deletion. We should either:
	// 1. Rename TestDirectoryDeletion to TestFileDeletionInSubdirectory
	// 2. Implement actual directory deletion in the MCP server (delete all files in dir)
	// 3. Create multiple files and verify all are deleted
	var deletionCommit *struct {
		SHA    string `json:"sha"`
		Commit struct {
			Message string `json:"message"`
		}
		Files []struct {
			Filename  string `json:"filename"`
			Deletions int    `json:"deletions"`
		} `json:"files"`
	}
	for i := range trimmedListCommitsText {
		if trimmedListCommitsText[i].Commit.Message == "Delete test directory" {
			deletionCommit = &trimmedListCommitsText[i]
			break
		}
	}
	require.NotNil(t, deletionCommit, "expected to find a commit with message 'Delete test directory'")

	// Now get the commit so we can look at the file changes because list_commits doesn't include them

	t.Logf("Getting commit %s/%s:%s...", currentOwner, repoName, deletionCommit.SHA)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "get_commit",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
			"sha":   deletionCommit.SHA,
		},
	})
	require.NoError(t, err, "expected to call 'get_commit' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetCommitText struct {
		Files []struct {
			Filename  string `json:"filename"`
			Deletions int    `json:"deletions"`
		}
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetCommitText)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	require.Len(t, trimmedGetCommitText.Files, 1, "expected to find one file change")
	require.Equal(t, "test-dir/test-file.txt", trimmedGetCommitText.Files[0].Filename, "expected filename to match")
	require.Equal(t, 1, trimmedGetCommitText.Files[0].Deletions, "expected one deletion")
}

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

	if getE2EHost() != "" && getE2EHost() != "https://github.com" {
		t.Skip("Skipping test because the host does not support copilot reviews")
	}

	mcpClient := setupMCPClient(t)
	ctx := context.Background()

	// First, who am I

	t.Log("Getting current user...")
	resp, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok := resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetMeText struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	currentOwner := trimmedGetMeText.Login

	// Then create a repository with a README (via autoInit)
	repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())

	t.Logf("Creating repository %s/%s...", currentOwner, repoName)
	_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_repository",
		Arguments: map[string]any{
			"name":     repoName,
			"private":  true,
			"autoInit": true,
		},
	})
	require.NoError(t, err, "expected to call 'create_repository' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Cleanup the repository after the test
	t.Cleanup(func() {
		// MCP Server doesn't support deletions, but we can use the GitHub Client
		ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
		t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
		_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
		require.NoError(t, err, "expected to delete repository successfully")
	})

	// Create a branch on which to create a new commit

	t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_branch",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"branch":      "test-branch",
			"from_branch": "main",
		},
	})
	require.NoError(t, err, "expected to call 'create_branch' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a commit with a new file

	t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_or_update_file",
		Arguments: map[string]any{
			"owner":   currentOwner,
			"repo":    repoName,
			"path":    "test-file.txt",
			"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
			"message": "Add test file",
			"branch":  "test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedCommitText struct {
		SHA string `json:"sha"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	commitID := trimmedCommitText.SHA

	// Create a pull request

	t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_pull_request",
		Arguments: map[string]any{
			"owner":    currentOwner,
			"repo":     repoName,
			"title":    "Test PR",
			"body":     "This is a test PR",
			"head":     "test-branch",
			"base":     "main",
			"commitID": commitID,
		},
	})
	require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Request a copilot review

	t.Logf("Requesting Copilot review for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "request_copilot_review",
		Arguments: map[string]any{
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'request_copilot_review' tool successfully")

	// Check if Copilot is available - skip if not
	if resp.IsError {
		if tc, ok := resp.Content[0].(*mcp.TextContent); ok {
			if strings.Contains(tc.Text, "copilot") || strings.Contains(tc.Text, "Copilot") {
				t.Skip("skipping because copilot isn't available as a reviewer on this repository")
			}
		}
		require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
	}

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")
	require.Equal(t, "", textContent.Text, "expected content to be empty")

	// Finally, get requested reviews and see copilot is in there
	// MCP Server doesn't support requesting reviews yet, but we can use the GitHub Client
	ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
	t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
	reviewRequests, _, err := ghClient.PullRequests.ListReviewers(context.Background(), currentOwner, repoName, 1, nil)
	require.NoError(t, err, "expected to get review requests successfully")

	// Check if Copilot was added as a reviewer - skip if not available
	if len(reviewRequests.Users) == 0 {
		t.Skip("skipping because copilot wasn't added as a reviewer (likely not enabled for this account)")
	}

	// Check that there is one review request from copilot
	require.Len(t, reviewRequests.Users, 1, "expected to find one review request")
	require.Equal(t, "Copilot", *reviewRequests.Users[0].Login, "expected review request to be for Copilot")
	require.Equal(t, "Bot", *reviewRequests.Users[0].Type, "expected review request to be for Bot")
}

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

	if getE2EHost() != "" && getE2EHost() != "https://github.com" {
		t.Skip("Skipping test because the host does not support copilot being assigned to issues")
	}

	mcpClient := setupMCPClient(t)
	ctx := context.Background()

	// First, who am I

	t.Log("Getting current user...")
	resp, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok := resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetMeText struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	currentOwner := trimmedGetMeText.Login

	// Then create a repository with a README (via autoInit)
	repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())

	t.Logf("Creating repository %s/%s...", currentOwner, repoName)
	_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_repository",
		Arguments: map[string]any{
			"name":     repoName,
			"private":  true,
			"autoInit": true,
		},
	})
	require.NoError(t, err, "expected to call 'create_repository' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Cleanup the repository after the test
	t.Cleanup(func() {
		// MCP Server doesn't support deletions, but we can use the GitHub Client
		ghClient := getRESTClient(t)
		t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
		_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
		require.NoError(t, err, "expected to delete repository successfully")
	})

	// Create an issue

	t.Logf("Creating issue in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "issue_write",
		Arguments: map[string]any{
			"method": "create",
			"owner":  currentOwner,
			"repo":   repoName,
			"title":  "Test issue to assign copilot to",
		},
	})
	require.NoError(t, err, "expected to call 'issue_write' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Assign copilot to the issue

	t.Logf("Assigning copilot to issue in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "assign_copilot_to_issue",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"issueNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'assign_copilot_to_issue' tool successfully")

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	possibleExpectedFailure := "copilot isn't available as an assignee for this issue. Please inform the user to visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot for more information."
	if resp.IsError && textContent.Text == possibleExpectedFailure {
		t.Skip("skipping because copilot wasn't available as an assignee on this issue, it's likely that the owner doesn't have copilot enabled in their settings")
	}

	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.Equal(t, "successfully assigned copilot to issue", textContent.Text)

	// Check that copilot is assigned to the issue
	// MCP Server doesn't support getting assignees yet
	ghClient := getRESTClient(t)
	assignees, response, err := ghClient.Issues.Get(context.Background(), currentOwner, repoName, 1)
	require.NoError(t, err, "expected to get issue successfully")
	require.Equal(t, http.StatusOK, response.StatusCode, "expected to get issue successfully")
	require.Len(t, assignees.Assignees, 1, "expected to find one assignee")
	require.Equal(t, "Copilot", *assignees.Assignees[0].Login, "expected copilot to be assigned to the issue")
}

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

	mcpClient := setupMCPClient(t)

	ctx := context.Background()

	// First, who am I

	t.Log("Getting current user...")
	resp, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok := resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetMeText struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	currentOwner := trimmedGetMeText.Login

	// Then create a repository with a README (via autoInit)
	repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())

	t.Logf("Creating repository %s/%s...", currentOwner, repoName)
	_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_repository",
		Arguments: map[string]any{
			"name":     repoName,
			"private":  true,
			"autoInit": true,
		},
	})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Cleanup the repository after the test
	t.Cleanup(func() {
		// MCP Server doesn't support deletions, but we can use the GitHub Client
		ghClient := getRESTClient(t)
		t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
		_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
		require.NoError(t, err, "expected to delete repository successfully")
	})

	// Create a branch on which to create a new commit

	t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_branch",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"branch":      "test-branch",
			"from_branch": "main",
		},
	})
	require.NoError(t, err, "expected to call 'create_branch' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a commit with a new file

	t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_or_update_file",
		Arguments: map[string]any{
			"owner":   currentOwner,
			"repo":    repoName,
			"path":    "test-file.txt",
			"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
			"message": "Add test file",
			"branch":  "test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedCommitText struct {
		Commit struct {
			SHA string `json:"sha"`
		} `json:"commit"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	commitID := trimmedCommitText.Commit.SHA

	// Create a pull request

	t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_pull_request",
		Arguments: map[string]any{
			"owner":    currentOwner,
			"repo":     repoName,
			"title":    "Test PR",
			"body":     "This is a test PR",
			"head":     "test-branch",
			"base":     "main",
			"commitID": commitID,
		},
	})
	require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create and submit a review

	t.Logf("Creating and submitting review for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_review_write",
		Arguments: map[string]any{
			"method":     "create",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
			"event":      "COMMENT", // the only event we can use as the creator of the PR
			"body":       "Looks good if you like bad code I guess!",
			"commitID":   commitID,
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_review_write' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Finally, get the list of reviews and see that our review has been submitted

	t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_read",
		Arguments: map[string]any{
			"method":     "get_reviews",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_read' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var reviews []struct {
		State string `json:"state"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &reviews)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	// Check that there is one review
	require.Len(t, reviews, 1, "expected to find one review")
	require.Equal(t, "COMMENTED", reviews[0].State, "expected review state to be COMMENTED")
}

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

	mcpClient := setupMCPClient(t)

	ctx := context.Background()

	// First, who am I

	t.Log("Getting current user...")
	resp, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok := resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetMeText struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	currentOwner := trimmedGetMeText.Login

	// Then create a repository with a README (via autoInit)
	repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())

	t.Logf("Creating repository %s/%s...", currentOwner, repoName)
	_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_repository",
		Arguments: map[string]any{
			"name":     repoName,
			"private":  true,
			"autoInit": true,
		},
	})
	require.NoError(t, err, "expected to call 'create_repository' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Cleanup the repository after the test
	t.Cleanup(func() {
		// MCP Server doesn't support deletions, but we can use the GitHub Client
		ghClient := getRESTClient(t)
		t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
		_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
		require.NoError(t, err, "expected to delete repository successfully")
	})

	// Create a branch on which to create a new commit

	t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_branch",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"branch":      "test-branch",
			"from_branch": "main",
		},
	})
	require.NoError(t, err, "expected to call 'create_branch' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a commit with a new file (multi-line content to support multi-line review comments)

	t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
	multiLineContent := fmt.Sprintf("Line 1: Created by e2e test %s\nLine 2: Additional content for multi-line comments\nLine 3: More content", t.Name())
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_or_update_file",
		Arguments: map[string]any{
			"owner":   currentOwner,
			"repo":    repoName,
			"path":    "test-file.txt",
			"content": multiLineContent,
			"message": "Add test file",
			"branch":  "test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedCommitText struct {
		Commit struct {
			SHA string `json:"sha"`
		} `json:"commit"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	commitID := trimmedCommitText.Commit.SHA

	// Create a pull request

	t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_pull_request",
		Arguments: map[string]any{
			"owner":    currentOwner,
			"repo":     repoName,
			"title":    "Test PR",
			"body":     "This is a test PR",
			"head":     "test-branch",
			"base":     "main",
			"commitID": commitID,
		},
	})
	require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a review for the pull request, but we can't approve it
	// because the current owner also owns the PR.

	t.Logf("Creating pending review for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_review_write",
		Arguments: map[string]any{
			"method":     "create",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_review_write' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")
	require.Equal(t, "pending pull request created", textContent.Text)

	// Add a file review comment
	// TODO: FILE-level comments are silently dropped by GitHub API when:
	// - The comment targets the wrong side of a diff
	// - The comment targets a deleted part of a diff
	// - The comment targets a line outside the actual diff range
	// This test currently doesn't verify FILE-level comments are created because
	// ListReviewComments API doesn't return them. We should investigate proper
	// FILE-level comment parameters or use a different API to verify.

	t.Logf("Adding file review comment to pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "add_comment_to_pending_review",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"pullNumber":  1,
			"path":        "test-file.txt",
			"subjectType": "FILE",
			"body":        "File review comment",
			"side":        "RIGHT",
		},
	})
	require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Add a single line review comment

	t.Logf("Adding single line review comment to pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "add_comment_to_pending_review",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"pullNumber":  1,
			"path":        "test-file.txt",
			"subjectType": "LINE",
			"body":        "Single line review comment",
			"line":        1,
			"side":        "RIGHT",
			"commitID":    commitID,
		},
	})
	require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Add a multiline review comment

	t.Logf("Adding multi line review comment to pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "add_comment_to_pending_review",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"pullNumber":  1,
			"path":        "test-file.txt",
			"subjectType": "LINE",
			"body":        "Multiline review comment",
			"startLine":   1,
			"line":        2,
			"startSide":   "RIGHT",
			"side":        "RIGHT",
			"commitID":    commitID,
		},
	})
	require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Submit the review

	t.Logf("Submitting review for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_review_write",
		Arguments: map[string]any{
			"method":     "submit_pending",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
			"event":      "COMMENT", // the only event we can use as the creator of the PR
			"body":       "Looks good if you like bad code I guess!",
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_review_write' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Finally, get the review and see that it has been created

	t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_read",
		Arguments: map[string]any{
			"method":     "get_reviews",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_read' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var reviews []struct {
		ID    int    `json:"id"`
		State string `json:"state"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &reviews)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	// Check that there is one review
	require.Len(t, reviews, 1, "expected to find one review")
	require.Equal(t, "COMMENTED", reviews[0].State, "expected review state to be COMMENTED")

	// Check that there are review comments
	// MCP Server doesn't support this, but we can use the GitHub Client
	// Note: FILE-level comments may not be returned by ListReviewComments API,
	// so we expect at least the LINE-level comments (single-line and multi-line)
	ghClient := getRESTClient(t)
	comments, _, err := ghClient.PullRequests.ListReviewComments(context.Background(), currentOwner, repoName, 1, int64(reviews[0].ID), nil)
	require.NoError(t, err, "expected to list review comments successfully")
	require.GreaterOrEqual(t, len(comments), 2, "expected to find at least two review comments (LINE-level)")
}

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

	mcpClient := setupMCPClient(t)

	ctx := context.Background()

	// First, who am I

	t.Log("Getting current user...")
	resp, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{Name: "get_me"})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	require.False(t, resp.IsError, "expected result not to be an error")
	require.Len(t, resp.Content, 1, "expected content to have one item")

	textContent, ok := resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var trimmedGetMeText struct {
		Login string `json:"login"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	currentOwner := trimmedGetMeText.Login

	// Then create a repository with a README (via autoInit)
	repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())

	t.Logf("Creating repository %s/%s...", currentOwner, repoName)
	_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_repository",
		Arguments: map[string]any{
			"name":     repoName,
			"private":  true,
			"autoInit": true,
		},
	})
	require.NoError(t, err, "expected to call 'get_me' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Cleanup the repository after the test
	t.Cleanup(func() {
		// MCP Server doesn't support deletions, but we can use the GitHub Client
		ghClient := getRESTClient(t)
		t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
		_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
		require.NoError(t, err, "expected to delete repository successfully")
	})

	// Create a branch on which to create a new commit

	t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_branch",
		Arguments: map[string]any{
			"owner":       currentOwner,
			"repo":        repoName,
			"branch":      "test-branch",
			"from_branch": "main",
		},
	})
	require.NoError(t, err, "expected to call 'create_branch' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a commit with a new file

	t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_or_update_file",
		Arguments: map[string]any{
			"owner":   currentOwner,
			"repo":    repoName,
			"path":    "test-file.txt",
			"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
			"message": "Add test file",
			"branch":  "test-branch",
		},
	})
	require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a pull request

	t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "create_pull_request",
		Arguments: map[string]any{
			"owner": currentOwner,
			"repo":  repoName,
			"title": "Test PR",
			"body":  "This is a test PR",
			"head":  "test-branch",
			"base":  "main",
		},
	})
	require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// Create a review for the pull request, but we can't approve it
	// because the current owner also owns the PR.

	t.Logf("Creating pending review for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_review_write",
		Arguments: map[string]any{
			"method":     "create",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_review_write' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")
	require.Equal(t, "pending pull request created", textContent.Text)

	// See that there is a pending review

	t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_read",
		Arguments: map[string]any{
			"method":     "get_reviews",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_read' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var reviews []struct {
		State string `json:"state"`
	}
	err = json.Unmarshal([]byte(textContent.Text), &reviews)
	require.NoError(t, err, "expected to unmarshal text content successfully")

	// Check that there is one review
	require.Len(t, reviews, 1, "expected to find one review")
	require.Equal(t, "PENDING", reviews[0].State, "expected review state to be PENDING")

	// Delete the review

	t.Logf("Deleting review for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_review_write",
		Arguments: map[string]any{
			"method":     "delete_pending",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_review_write' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	// See that there are no reviews
	t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
	resp, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
		Name: "pull_request_read",
		Arguments: map[string]any{
			"method":     "get_reviews",
			"owner":      currentOwner,
			"repo":       repoName,
			"pullNumber": 1,
		},
	})
	require.NoError(t, err, "expected to call 'pull_request_read' tool successfully")
	require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))

	textContent, ok = resp.Content[0].(*mcp.TextContent)
	require.True(t, ok, "expected content to be of type TextContent")

	var noReviews []struct{}
	err = json.Unmarshal([]byte(textContent.Text), &noReviews)
	require.NoError(t, err, "expected to unmarshal text content successfully")
	require.Len(t, noReviews, 0, "expected to find no reviews")
}
