package git

import (
	"os"
	"path/filepath"
	"slices"
	"testing"

	"github.com/stretchr/testify/require"
	knotconfig "tangled.org/core/knotserver/config"
)

// TestValidateSource guards against the argument-injection / RCE reported for
// sh.tangled.repo.create: a source beginning with "--" (e.g.
// "--upload-pack=<cmd>") was handed straight to `git clone`, which interpreted
// it as an option and executed the embedded command as the git user.
func TestValidateSource(t *testing.T) {
	rejected := []struct {
		name   string
		source string
	}{
		{"upload-pack injection", "--upload-pack=touch /tmp/pwned"},
		{"upload-pack injection with IFS", "--upload-pack=touch$IFS/tmp/pwned"},
		{"leading dash", "-oProxyCommand=evil"},
		{"leading dash single", "-"},
		{"ext transport", "ext::sh -c 'touch /tmp/pwned'"},
		{"file scheme", "file:///etc/passwd"},
		{"ssh scheme", "ssh://git@example.com/repo"},
		{"git scheme", "git://example.com/repo"},
		{"scp-like syntax", "git@example.com:repo.git"},
		{"empty", ""},
		{"relative path", "some/local/path"},
		{"missing host", "https://"},
	}

	for _, tc := range rejected {
		t.Run("reject/"+tc.name, func(t *testing.T) {
			_, err := validateSource(tc.source)
			require.Error(t, err, "source %q must be rejected", tc.source)
		})
	}

	accepted := []struct {
		name   string
		source string
	}{
		{"https", "https://example.com/owner/repo"},
		{"http", "http://example.com/owner/repo"},
		{"https with port", "https://example.com:8443/owner/repo.git"},
		{"https with userinfo", "https://user:token@example.com/owner/repo"},
	}

	for _, tc := range accepted {
		t.Run("accept/"+tc.name, func(t *testing.T) {
			u, err := validateSource(tc.source)
			require.NoError(t, err, "source %q must be accepted", tc.source)
			require.NotNil(t, u)
		})
	}
}

// TestCloneArgs asserts the hardening flags are present on the clone command:
// the "--" option terminator and a deny-by-default protocol allowlist.
func TestCloneArgs(t *testing.T) {
	t.Run("remote http source", func(t *testing.T) {
		u, err := validateSource("https://example.com/owner/repo")
		require.NoError(t, err)

		args := cloneArgs(u, "/scan/did:plc:abc", false)

		requireOrdered(t, args, "clone", "--bare", "--", "https://example.com/owner/repo", "/scan/did:plc:abc")
		requireConfig(t, args, "protocol.allow=never")
		requireConfig(t, args, "protocol.http.allow=always")
		requireConfig(t, args, "protocol.https.allow=always")
		// the file transport must NOT be enabled for a user-provided source.
		require.False(t, hasConfig(args, "protocol.file.allow=always"),
			"file transport must not be allowlisted for remote sources")

		// the source is the operand immediately after "--", so a leading-dash
		// payload can never be read as a flag.
		dashIdx := slices.Index(args, "--")
		require.Positive(t, dashIdx)
		require.Equal(t, "https://example.com/owner/repo", args[dashIdx+1])
	})

	t.Run("local optimized clone allowlists file", func(t *testing.T) {
		u, err := validateSource("https://example.com/owner/repo")
		require.NoError(t, err)

		args := cloneArgs(u, "/scan/did:plc:abc", true)
		requireConfig(t, args, "protocol.file.allow=always")
	})
}

// TestForkWithSandboxRejectsInjection is the end-to-end regression: the PoC
// payload must fail before any command runs, and the sentinel file the payload
// would create must never appear.
func TestForkWithSandboxRejectsInjection(t *testing.T) {
	tempDir := t.TempDir()
	sentinel := filepath.Join(tempDir, "pwned")

	cfg := &knotconfig.Config{}
	cfg.Repo.ScanPath = filepath.Join(tempDir, "scan")
	cfg.Server.Hostname = "knot.example.com"

	repoPath := filepath.Join(cfg.Repo.ScanPath, "did:plc:victim")

	// mirrors the PoC: --upload-pack=touch <sentinel>, spaces as $IFS.
	source := "--upload-pack=touch$IFS" + sentinel

	err := ForkWithSandbox(repoPath, source, cfg, nil)
	require.Error(t, err, "malicious source must be rejected")

	_, statErr := os.Stat(sentinel)
	require.True(t, os.IsNotExist(statErr),
		"command injection executed: sentinel file %s was created", sentinel)
}

func hasConfig(args []string, kv string) bool {
	for i := 0; i+1 < len(args); i++ {
		if args[i] == "-c" && args[i+1] == kv {
			return true
		}
	}
	return false
}

func requireConfig(t *testing.T, args []string, kv string) {
	t.Helper()
	require.True(t, hasConfig(args, kv), "expected -c %s in %v", kv, args)
}

// requireOrdered asserts want appears as a subsequence (in order) of args.
func requireOrdered(t *testing.T, args []string, want ...string) {
	t.Helper()
	i := 0
	for _, a := range args {
		if i < len(want) && a == want[i] {
			i++
		}
	}
	require.Equal(t, len(want), i, "expected ordered subsequence %v in %v", want, args)
}
