// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package runner

import (
	"context"
	"strings"
	"testing"

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

	"github.com/stacklok/toolhive/pkg/container/templates"
)

func TestIsLocalGoPath(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name     string
		path     string
		expected bool
	}{
		{
			name:     "current directory",
			path:     ".",
			expected: true,
		},
		{
			name:     "relative path with ./",
			path:     "./cmd/server",
			expected: true,
		},
		{
			name:     "relative path with ../",
			path:     "../other-project",
			expected: true,
		},
		{
			name:     "absolute path",
			path:     "/home/user/project",
			expected: true,
		},
		{
			name:     "remote package",
			path:     "github.com/example/package",
			expected: false,
		},
		{
			name:     "remote package with version",
			path:     "github.com/example/package@v1.0.0",
			expected: false,
		},
		{
			name:     "simple package name",
			path:     "mypackage",
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			result := isLocalGoPath(tt.path)
			if result != tt.expected {
				t.Errorf("isLocalGoPath(%q) = %v, want %v", tt.path, result, tt.expected)
			}
		})
	}
}

func TestPackageNameToImageName(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{
			name:     "simple package name",
			input:    "mypackage",
			expected: "mypackage",
		},
		{
			name:     "package with slashes",
			input:    "github.com/user/repo",
			expected: "github-com-user-repo",
		},
		{
			name:     "package with version",
			input:    "github.com/user/repo@v1.0.0",
			expected: "github-com-user-repo-v1-0-0",
		},
		{
			name:     "relative path with ./",
			input:    "./cmd/server",
			expected: "cmd-server",
		},
		{
			name:     "relative path with ../",
			input:    "../other-project",
			expected: "other-project",
		},
		{
			name:     "current directory",
			input:    ".",
			expected: "toolhive-container",
		},
		{
			name:     "path with dots",
			input:    "./my.project",
			expected: "my-project",
		},
		{
			name:     "complex path",
			input:    "./cmd/my.server/main",
			expected: "cmd-my-server-main",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			result := PackageNameToImageName(tt.input)
			if result != tt.expected {
				t.Errorf("PackageNameToImageName(%q) = %q, want %q", tt.input, result, tt.expected)
			}
		})
	}
}

func TestIsImageProtocolScheme(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name     string
		input    string
		expected bool
	}{
		{
			name:     "uvx scheme",
			input:    "uvx://package-name",
			expected: true,
		},
		{
			name:     "npx scheme",
			input:    "npx://package-name",
			expected: true,
		},
		{
			name:     "go scheme",
			input:    "go://package-name",
			expected: true,
		},
		{
			name:     "go scheme with local path",
			input:    "go://./cmd/server",
			expected: true,
		},
		{
			name:     "regular image name",
			input:    "docker.io/library/alpine:latest",
			expected: false,
		},
		{
			name:     "registry server name",
			input:    "fetch",
			expected: false,
		},
		{
			name:     "empty string",
			input:    "",
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			result := IsImageProtocolScheme(tt.input)
			if result != tt.expected {
				t.Errorf("IsImageProtocolScheme(%q) = %v, want %v", tt.input, result, tt.expected)
			}
		})
	}
}

func TestTemplateDataWithLocalPath(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name        string
		packageName string
		expected    templates.TemplateData
	}{
		{
			name:        "remote package",
			packageName: "github.com/example/package",
			expected: templates.TemplateData{
				MCPPackage:  "github.com/example/package",
				IsLocalPath: false,
				BuildArgs:   nil,
			},
		},
		{
			name:        "local relative path",
			packageName: "./cmd/server",
			expected: templates.TemplateData{
				MCPPackage:  "./cmd/server",
				IsLocalPath: true,
				BuildArgs:   nil,
			},
		},
		{
			name:        "current directory",
			packageName: ".",
			expected: templates.TemplateData{
				MCPPackage:  ".",
				IsLocalPath: true,
				BuildArgs:   nil,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			// Test the logic that would be used in HandleProtocolScheme
			isLocalPath := isLocalGoPath(tt.packageName)

			templateData := templates.TemplateData{
				MCPPackage:  tt.packageName,
				IsLocalPath: isLocalPath,
				BuildArgs:   nil,
			}

			if templateData.MCPPackage != tt.expected.MCPPackage {
				t.Errorf("MCPPackage = %q, want %q", templateData.MCPPackage, tt.expected.MCPPackage)
			}
			if templateData.IsLocalPath != tt.expected.IsLocalPath {
				t.Errorf("IsLocalPath = %v, want %v", templateData.IsLocalPath, tt.expected.IsLocalPath)
			}
		})
	}
}

func TestBuildFromProtocolSchemeWithNameDryRun(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name          string
		serverOrImage string
		caCertPath    string
		buildArgs     []string
		wantContains  []string
		wantErr       bool
	}{
		{
			name:          "NPX with buildArgs in dry-run",
			serverOrImage: "npx://@launchdarkly/mcp-server",
			buildArgs:     []string{"start"},
			wantContains: []string{
				`ENTRYPOINT ["npx", "@launchdarkly/mcp-server", "start"]`,
				"FROM node:24-alpine",
			},
			wantErr: false,
		},
		{
			name:          "UVX with multiple buildArgs in dry-run",
			serverOrImage: "uvx://example-package",
			buildArgs:     []string{"--transport", "stdio"},
			wantContains: []string{
				"example-package",
				"--transport",
				"stdio",
				"FROM python:3.14-slim",
			},
			wantErr: false,
		},
		{
			name:          "GO with buildArgs in dry-run",
			serverOrImage: "go://github.com/example/package",
			buildArgs:     []string{"serve"},
			wantContains: []string{
				`ENTRYPOINT ["/app/mcp-server", "serve"]`,
			},
			wantErr: false,
		},
		{
			name:          "NPX with buildArgs and invalid CA cert path",
			serverOrImage: "npx://@launchdarkly/mcp-server",
			caCertPath:    "/nonexistent/ca-cert.crt",
			buildArgs:     []string{"start"},
			wantErr:       true,
		},
	}

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

			// Call BuildFromProtocolSchemeWithName with dry-run=true
			dockerfileContent, err := BuildFromProtocolSchemeWithName(
				ctx, nil, tt.serverOrImage, tt.caCertPath, "", tt.buildArgs, nil, true)

			if (err != nil) != tt.wantErr {
				t.Errorf("BuildFromProtocolSchemeWithName() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if err == nil {
				for _, want := range tt.wantContains {
					if !strings.Contains(dockerfileContent, want) {
						t.Errorf("Dockerfile does not contain expected string %q", want)
					}
				}
			}
		})
	}
}

func TestMergeRuntimeConfig(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name         string
		transport    templates.TransportType
		override     *templates.RuntimeConfig
		wantImage    string
		wantPackages []string
	}{
		{
			name:      "only packages override, no image — image falls back to default",
			transport: templates.TransportTypeNPX,
			override: &templates.RuntimeConfig{
				BuilderImage:       "",
				AdditionalPackages: []string{"curl"},
			},
			wantImage:    "node:24-alpine",
			wantPackages: []string{"git", "ca-certificates", "curl"},
		},
		{
			name:      "both image and packages override — image from override, packages merged",
			transport: templates.TransportTypeNPX,
			override: &templates.RuntimeConfig{
				BuilderImage:       "node:20-alpine",
				AdditionalPackages: []string{"curl"},
			},
			wantImage:    "node:20-alpine",
			wantPackages: []string{"git", "ca-certificates", "curl"},
		},
		{
			name:      "duplicate package in override — not added twice",
			transport: templates.TransportTypeNPX,
			override: &templates.RuntimeConfig{
				BuilderImage:       "",
				AdditionalPackages: []string{"git"},
			},
			wantImage:    "node:24-alpine",
			wantPackages: []string{"git", "ca-certificates"},
		},
		{
			name:      "empty override — all defaults",
			transport: templates.TransportTypeNPX,
			override: &templates.RuntimeConfig{
				BuilderImage:       "",
				AdditionalPackages: nil,
			},
			wantImage:    "node:24-alpine",
			wantPackages: []string{"git", "ca-certificates"},
		},
		{
			name:      "go transport — defaults apply",
			transport: templates.TransportTypeGO,
			override: &templates.RuntimeConfig{
				BuilderImage:       "",
				AdditionalPackages: []string{"make"},
			},
			wantImage:    "golang:1.26-alpine",
			wantPackages: []string{"ca-certificates", "git", "make"},
		},
		{
			name:      "uvx transport — defaults apply",
			transport: templates.TransportTypeUVX,
			override: &templates.RuntimeConfig{
				BuilderImage:       "",
				AdditionalPackages: []string{"curl"},
			},
			wantImage:    "python:3.14-slim",
			wantPackages: []string{"ca-certificates", "git", "curl"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			got := mergeRuntimeConfig(tt.transport, tt.override)

			if got.BuilderImage != tt.wantImage {
				t.Errorf("BuilderImage = %q, want %q", got.BuilderImage, tt.wantImage)
			}
			if len(got.AdditionalPackages) != len(tt.wantPackages) {
				t.Errorf("AdditionalPackages = %v, want %v", got.AdditionalPackages, tt.wantPackages)
			} else {
				for i, pkg := range tt.wantPackages {
					if got.AdditionalPackages[i] != pkg {
						t.Errorf("AdditionalPackages[%d] = %q, want %q", i, got.AdditionalPackages[i], pkg)
					}
				}
			}
		})
	}
}

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

	// Simulate the bug: --runtime-add-package without --runtime-image
	override := &templates.RuntimeConfig{
		BuilderImage:       "",
		AdditionalPackages: []string{"curl"},
	}

	got, err := loadRuntimeConfig(templates.TransportTypeNPX, override)
	if err != nil {
		t.Fatalf("loadRuntimeConfig() error = %v", err)
	}

	if got.BuilderImage == "" {
		t.Error("loadRuntimeConfig() returned empty BuilderImage — should fall back to default")
	}
	if got.BuilderImage != "node:24-alpine" {
		t.Errorf("BuilderImage = %q, want %q", got.BuilderImage, "node:24-alpine")
	}
}

func TestCreateTemplateData(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name          string
		transportType templates.TransportType
		packageName   string
		caCertPath    string
		buildArgs     []string
		expected      templates.TemplateData
		wantErr       bool
	}{
		{
			name:          "NPX with buildArgs",
			transportType: templates.TransportTypeNPX,
			packageName:   "@launchdarkly/mcp-server",
			caCertPath:    "",
			buildArgs:     []string{"start"},
			expected: templates.TemplateData{
				MCPPackage:  "@launchdarkly/mcp-server",
				IsLocalPath: false,
				BuildArgs:   []string{"start"},
			},
			wantErr: false,
		},
		{
			name:          "UVX with multiple buildArgs",
			transportType: templates.TransportTypeUVX,
			packageName:   "example-package",
			caCertPath:    "",
			buildArgs:     []string{"--transport", "stdio"},
			expected: templates.TemplateData{
				MCPPackage:  "example-package",
				IsLocalPath: false,
				BuildArgs:   []string{"--transport", "stdio"},
			},
			wantErr: false,
		},
		{
			name:          "GO with buildArgs",
			transportType: templates.TransportTypeGO,
			packageName:   "github.com/example/package",
			caCertPath:    "",
			buildArgs:     []string{"serve", "--verbose"},
			expected: templates.TemplateData{
				MCPPackage:  "github.com/example/package",
				IsLocalPath: false,
				BuildArgs:   []string{"serve", "--verbose"},
			},
			wantErr: false,
		},
		{
			name:          "GO local path with buildArgs",
			transportType: templates.TransportTypeGO,
			packageName:   "./cmd/server",
			caCertPath:    "",
			buildArgs:     []string{"--config", "config.yaml"},
			expected: templates.TemplateData{
				MCPPackage:  "./cmd/server",
				IsLocalPath: true,
				BuildArgs:   []string{"--config", "config.yaml"},
			},
			wantErr: false,
		},
		{
			name:          "NPX without buildArgs",
			transportType: templates.TransportTypeNPX,
			packageName:   "package-name",
			caCertPath:    "",
			buildArgs:     nil,
			expected: templates.TemplateData{
				MCPPackage:  "package-name",
				IsLocalPath: false,
				BuildArgs:   nil,
			},
			wantErr: false,
		},
		{
			name:          "buildArgs with single quote should fail",
			transportType: templates.TransportTypeUVX,
			packageName:   "example-package",
			caCertPath:    "",
			buildArgs:     []string{"--name", "test'arg"},
			expected:      templates.TemplateData{},
			wantErr:       true,
		},
		{
			name:          "buildArgs with other special characters should succeed",
			transportType: templates.TransportTypeNPX,
			packageName:   "example-package",
			caCertPath:    "",
			buildArgs:     []string{"--config", "file$with`special\"chars"},
			expected: templates.TemplateData{
				MCPPackage:  "example-package",
				IsLocalPath: false,
				BuildArgs:   []string{"--config", "file$with`special\"chars"},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			result, err := createTemplateData(tt.transportType, tt.packageName, tt.caCertPath, tt.buildArgs, nil)

			if (err != nil) != tt.wantErr {
				t.Errorf("createTemplateData() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if result.MCPPackage != tt.expected.MCPPackage {
				t.Errorf("MCPPackage = %q, want %q", result.MCPPackage, tt.expected.MCPPackage)
			}
			if result.IsLocalPath != tt.expected.IsLocalPath {
				t.Errorf("IsLocalPath = %v, want %v", result.IsLocalPath, tt.expected.IsLocalPath)
			}
			if len(result.BuildArgs) != len(tt.expected.BuildArgs) {
				t.Errorf("BuildArgs length = %d, want %d", len(result.BuildArgs), len(tt.expected.BuildArgs))
			} else {
				for i, arg := range result.BuildArgs {
					if arg != tt.expected.BuildArgs[i] {
						t.Errorf("BuildArgs[%d] = %q, want %q", i, arg, tt.expected.BuildArgs[i])
					}
				}
			}
		})
	}
}

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

	base, err := loadRuntimeConfig(templates.TransportTypeGO, nil)
	require.NoError(t, err)
	require.NotNil(t, base)
	assert.NotEmpty(t, base.BuilderImage)
}

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

	base, err := loadRuntimeConfig(templates.TransportTypeGO, nil)
	require.NoError(t, err)
	require.NotNil(t, base)

	override := &templates.RuntimeConfig{
		BuilderImage:       "golang:1.24-alpine",
		AdditionalPackages: []string{"curl"},
	}
	got, err := loadRuntimeConfig(templates.TransportTypeGO, override)
	require.NoError(t, err)
	require.NotNil(t, got)
	assert.Equal(t, override.BuilderImage, got.BuilderImage)
	expectedPackages := append([]string{}, base.AdditionalPackages...)
	expectedPackages = append(expectedPackages, "curl")
	assert.Equal(t, expectedPackages, got.AdditionalPackages)

	// Ensure the returned config is detached from input slices.
	override.AdditionalPackages[0] = "git"
	assert.Equal(t, expectedPackages, got.AdditionalPackages)
}

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

	base, err := loadRuntimeConfig(templates.TransportTypeGO, nil)
	require.NoError(t, err)
	require.NotNil(t, base)

	customImage := "golang:1.24-alpine"
	got, err := loadRuntimeConfig(templates.TransportTypeGO, &templates.RuntimeConfig{
		BuilderImage: customImage,
	})
	require.NoError(t, err)
	require.NotNil(t, got)
	assert.Equal(t, customImage, got.BuilderImage)
	assert.Equal(t, base.AdditionalPackages, got.AdditionalPackages)
}
