package runtimecredential

import (
	"errors"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
)

func TestCredentialCreateReplaceAndClose(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	credential, err := Create("old-token")
	require.NoError(t, err)
	path := credential.Path()
	require.NotEmpty(t, path)
	require.Equal(t, profile.RuntimeDir(), filepath.Dir(filepath.Dir(path)))

	assertCredentialValue(t, path, "old-token")
	require.NoError(t, credential.Replace("new-token"))
	assertCredentialValue(t, path, "new-token")
	if runtime.GOOS != "windows" {
		fileInfo, statErr := os.Stat(path)
		require.NoError(t, statErr)
		require.Equal(t, os.FileMode(0o600), fileInfo.Mode().Perm())
		dirInfo, statErr := os.Stat(filepath.Dir(path))
		require.NoError(t, statErr)
		require.Equal(t, os.FileMode(0o700), dirInfo.Mode().Perm())
	}

	require.NoError(t, credential.Close())
	require.Empty(t, credential.Path())
	_, err = os.Lstat(path)
	require.ErrorIs(t, err, os.ErrNotExist)
	require.NoError(t, credential.Close())
	require.Error(t, credential.Replace("late-token"))
}

func TestCredentialCloseRemovesFailedPublicationResidue(t *testing.T) {
	t.Setenv("AGENTTEAM_CONFIG_DIR", t.TempDir())
	credential, err := Create("initial-token")
	require.NoError(t, err)
	dir := filepath.Dir(credential.Path())
	residue := filepath.Join(dir, ".token.tmp-orphan")
	require.NoError(t, os.WriteFile(residue, []byte("candidate-token\n"), 0o600))

	require.NoError(t, credential.Close())
	require.NoDirExists(t, dir)
}

func TestCredentialCloseRecursivelyRemovesOwnerManagedUnknownEntries(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	credential, err := Create("initial-token")
	require.NoError(t, err)
	dir := filepath.Dir(credential.Path())
	unknownDir := filepath.Join(dir, "runtime-created", "nested")
	require.NoError(t, os.MkdirAll(unknownDir, 0o700))
	require.NoError(t, os.WriteFile(filepath.Join(unknownDir, "unknown"), []byte("residue"), 0o600))

	require.NoError(t, credential.Close())
	require.NoDirExists(t, dir)
	require.NoError(t, credential.Close(), "recursive cleanup must remain idempotent")
}

func TestCredentialCloseDoesNotFollowDirectorySymlink(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("ordinary Windows test environments may not permit symlink creation")
	}
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	credential, err := Create("initial-token")
	require.NoError(t, err)
	dir := filepath.Dir(credential.Path())
	externalDir := t.TempDir()
	sentinel := filepath.Join(externalDir, "keep")
	require.NoError(t, os.WriteFile(sentinel, []byte("outside"), 0o600))
	require.NoError(t, os.Symlink(externalDir, filepath.Join(dir, "external-link")))

	require.NoError(t, credential.Close())
	require.NoDirExists(t, dir)
	require.FileExists(t, sentinel, "recursive cleanup must unlink, not traverse, directory symlinks")
}

func TestCredentialCloseDoesNotFollowReplacedOwnerDirectorySymlink(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("ordinary Windows test environments may not permit symlink creation")
	}
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	credential, err := Create("initial-token")
	require.NoError(t, err)
	dir := filepath.Dir(credential.Path())
	externalDir := t.TempDir()
	externalToken := filepath.Join(externalDir, credentialFilename)
	require.NoError(t, os.WriteFile(externalToken, []byte("outside"), 0o600))

	require.NoError(t, os.RemoveAll(dir))
	require.NoError(t, os.Symlink(externalDir, dir))
	require.NoError(t, credential.Close())
	require.NoFileExists(t, dir, "recursive cleanup must unlink the replaced owner-directory symlink")
	require.FileExists(t, externalToken, "cleanup must not traverse a replaced owner-directory symlink")
}

func TestCredentialConcurrentReadersSeeOnlyCompleteGenerations(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("Windows replacement sharing behavior is covered by native CI")
	}
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	first := strings.Repeat("a", 2048)
	second := strings.Repeat("b", 2048)
	credential, err := Create(first)
	require.NoError(t, err)
	t.Cleanup(func() { _ = credential.Close() })

	stop := make(chan struct{})
	errCh := make(chan error, 1)
	var readers sync.WaitGroup
	for range 4 {
		readers.Add(1)
		go func() {
			defer readers.Done()
			for {
				select {
				case <-stop:
					return
				default:
				}
				value, readErr := os.ReadFile(credential.Path())
				if readErr != nil {
					select {
					case errCh <- readErr:
					default:
					}
					return
				}
				trimmed := strings.TrimSpace(string(value))
				if trimmed != first && trimmed != second {
					select {
					case errCh <- errors.New("reader observed a partial credential"):
					default:
					}
					return
				}
			}
		}()
	}
	for index := 0; index < 100; index++ {
		value := first
		if index%2 == 0 {
			value = second
		}
		require.NoError(t, credential.Replace(value))
	}
	close(stop)
	readers.Wait()
	select {
	case readErr := <-errCh:
		require.NoError(t, readErr)
	default:
	}
}

func TestCleanupStaleRemovesOnlyExecCredentialDirectories(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	root, err := profile.EnsureRuntimeDir()
	require.NoError(t, err)
	staleOne := filepath.Join(root, credentialDirPrefix+"one")
	staleTwo := filepath.Join(root, credentialDirPrefix+"two")
	unrelated := filepath.Join(root, "keep-me")
	for _, path := range []string{staleOne, staleTwo, unrelated} {
		require.NoError(t, os.MkdirAll(path, 0o700))
		require.NoError(t, os.WriteFile(filepath.Join(path, "token"), []byte("dead-token"), 0o600))
	}

	require.NoError(t, CleanupStale())
	require.NoDirExists(t, staleOne)
	require.NoDirExists(t, staleTwo)
	require.DirExists(t, unrelated)
}

func assertCredentialValue(t *testing.T, path, want string) {
	t.Helper()
	value, err := os.ReadFile(path)
	require.NoError(t, err)
	require.Equal(t, want, strings.TrimSpace(string(value)))
}
