package userauth

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestProbeExposesPingCAPAuthentication(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		require.Equal(t, "/api/v1/public/config", request.URL.Path)
		writer.Header().Set("Content-Type", "application/json")
		_, _ = writer.Write([]byte(`{"pingcapAuthEnabled":true,"passwordAuthEnabled":false,"registrationMode":"invite_only"}`))
	}))
	t.Cleanup(server.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	result, err := manager.Probe(context.Background(), server.URL)
	require.NoError(t, err)
	require.True(t, result.PingCAPAuthEnabled)
	require.False(t, result.PasswordAuthEnabled)
}

func TestRegisterSendsDesktopOnboardingOptIn(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	var registrationRequest map[string]any
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/auth/register":
			require.NoError(t, json.NewDecoder(request.Body).Decode(&registrationRequest))
			_, _ = writer.Write([]byte(`{"accessToken":"access-secret","refreshToken":"refresh-secret","user":{"id":"usr_1","email":"user@example.com","name":"User","emailVerified":true}}`))
		case "/api/v1/workspaces":
			_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(server.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	_, err = manager.Register(context.Background(), server.URL, "User", "user@example.com", "password", true, "2026-08-09T00:00:00Z")
	require.NoError(t, err)
	require.Equal(t, true, registrationRequest["desktopOnboardingEnabled"])
	require.Equal(t, "2026-08-09T00:00:00Z", registrationRequest["desktopOnboardingRegistrationCutoff"])
}

func TestProbeHidesCrossOriginRedirectDetails(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		require.Equal(t, "/api/v1/public/config", request.URL.Path)
		http.Redirect(writer, request, "https://localhost/api/v1/public/config", http.StatusTemporaryRedirect)
	}))
	t.Cleanup(server.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	_, err = manager.Probe(context.Background(), server.URL)
	require.Error(t, err)
	require.NotContains(t, err.Error(), "cross-origin redirect")
	code, ok := ServerURLErrorCode(err)
	require.True(t, ok)
	require.Equal(t, serverURLErrorNotAgentTeam, code)
}

func TestProbeReportsNonAgentTeamServerWithUserFacingCode(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		require.Equal(t, "/api/v1/public/config", request.URL.Path)
		writer.Header().Set("Content-Type", "text/html")
		_, _ = writer.Write([]byte(`<html>not agentteam</html>`))
	}))
	t.Cleanup(server.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	_, err = manager.Probe(context.Background(), server.URL)
	require.Error(t, err)
	code, ok := ServerURLErrorCode(err)
	require.True(t, ok)
	require.Equal(t, serverURLErrorNotAgentTeam, code)
}

func TestProbeReportsTimeoutAsUnreachable(t *testing.T) {
	requestStarted := make(chan struct{})
	server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) {
		close(requestStarted)
		<-request.Context().Done()
	}))
	t.Cleanup(server.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	probeDone := make(chan error, 1)
	go func() {
		_, probeErr := manager.Probe(ctx, server.URL)
		probeDone <- probeErr
	}()
	<-requestStarted
	err = <-probeDone
	require.Error(t, err)
	code, ok := ServerURLErrorCode(err)
	require.True(t, ok)
	require.Equal(t, serverURLErrorUnreachable, code)
	require.NotContains(t, err.Error(), "context deadline exceeded")
}

func TestProbePreservesCallerCancellation(t *testing.T) {
	requestStarted := make(chan struct{})
	server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) {
		close(requestStarted)
		<-request.Context().Done()
	}))
	t.Cleanup(server.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	ctx, cancel := context.WithCancel(context.Background())
	probeDone := make(chan error, 1)
	go func() {
		_, probeErr := manager.Probe(ctx, server.URL)
		probeDone <- probeErr
	}()
	<-requestStarted
	cancel()

	err = <-probeDone
	require.ErrorIs(t, err, context.Canceled)
	_, ok := ServerURLErrorCode(err)
	require.False(t, ok)
}

func TestPingCAPAuthFlowKeepsPKCESecretsInsideDaemon(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	var callbackRequest map[string]any
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/public/config":
			_, _ = writer.Write([]byte(`{"pingcapAuthEnabled":true,"passwordAuthEnabled":false}`))
		case "/api/v1/auth/oidc/authorize-url":
			var body map[string]string
			require.NoError(t, json.NewDecoder(request.Body).Decode(&body))
			_, _ = writer.Write([]byte(`{"authorization_url":"https://auth.example.test/login","state":"state-secret","nonce":"nonce-secret","code_verifier":"verifier-secret"}`))
			require.True(t, strings.HasPrefix(body["redirect_uri"], "http://127.0.0.1:"))
		case "/api/v1/auth/oidc/callback":
			require.NoError(t, json.NewDecoder(request.Body).Decode(&callbackRequest))
			_, _ = writer.Write([]byte(`{"accessToken":"access-secret","refreshToken":"refresh-secret","user":{"id":"usr_1","email":"user@example.com","name":"User","emailVerified":true}}`))
		case "/api/v1/workspaces":
			require.Equal(t, "Bearer access-secret", request.Header.Get("Authorization"))
			_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(upstream.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	start, err := manager.BeginPingCAPAuth(context.Background(), upstream.URL, true, "2026-08-09T00:00:00Z")
	require.NoError(t, err)
	require.NotEmpty(t, start.FlowID)
	require.NotContains(t, start.FlowID, "secret")
	require.Equal(t, "https://auth.example.test/login", start.AuthorizationURL)

	manager.oidcMu.Lock()
	flow := manager.oidcFlows[start.FlowID]
	callbackURL := flow.redirectURI + "?code=authorization-code&state=state-secret"
	manager.oidcMu.Unlock()
	response, err := http.Get(callbackURL)
	require.NoError(t, err)
	_ = response.Body.Close()
	require.Equal(t, http.StatusOK, response.StatusCode)

	status, err := manager.PingCAPAuthFlowStatus(start.FlowID)
	require.NoError(t, err)
	require.Equal(t, "completed", status.Status)
	require.NotNil(t, status.Result)
	require.Len(t, status.Result.Workspaces, 1)
	require.Equal(t, "authorization-code", callbackRequest["code"])
	require.Equal(t, "nonce-secret", callbackRequest["nonce"])
	require.Equal(t, "verifier-secret", callbackRequest["code_verifier"])
	require.Equal(t, true, callbackRequest["desktopOnboardingEnabled"])
	require.Equal(t, "2026-08-09T00:00:00Z", callbackRequest["desktopOnboardingRegistrationCutoff"])

	data, err := os.ReadFile(filepath.Join(manager.configDir, authFileName))
	require.NoError(t, err)
	require.NotContains(t, string(data), "access-secret")
	require.NotContains(t, string(data), "refresh-secret")
	require.NotContains(t, string(data), "verifier-secret")
	_, err = manager.PingCAPAuthFlowStatus(start.FlowID)
	require.Error(t, err)
}

func TestPingCAPAuthRejectsWrongStateAndReplay(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/public/config":
			_, _ = writer.Write([]byte(`{"pingcapAuthEnabled":true}`))
		case "/api/v1/auth/oidc/authorize-url":
			_, _ = writer.Write([]byte(`{"authorization_url":"https://auth.example.test/login","state":"correct-state","nonce":"nonce","code_verifier":"verifier"}`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(upstream.Close)
	manager, err := New(t.TempDir())
	require.NoError(t, err)
	start, err := manager.BeginPingCAPAuth(context.Background(), upstream.URL, false, "")
	require.NoError(t, err)
	manager.oidcMu.Lock()
	callbackURL := manager.oidcFlows[start.FlowID].redirectURI
	manager.oidcMu.Unlock()

	response, err := http.Get(callbackURL + "?code=code&state=wrong-state")
	require.NoError(t, err)
	_ = response.Body.Close()
	require.Equal(t, http.StatusOK, response.StatusCode)
	status, err := manager.PingCAPAuthFlowStatus(start.FlowID)
	require.NoError(t, err)
	require.Equal(t, "error", status.Status)

	_, err = http.Get(callbackURL + "?code=code&state=correct-state")
	require.Error(t, err)
}

func TestCancelPingCAPAuthDuringExchangeDoesNotPersistSession(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	exchangeStarted := make(chan struct{})
	releaseExchange := make(chan struct{})
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/public/config":
			_, _ = writer.Write([]byte(`{"pingcapAuthEnabled":true}`))
		case "/api/v1/auth/oidc/authorize-url":
			_, _ = writer.Write([]byte(`{"authorization_url":"https://auth.example.test/login","state":"state","nonce":"nonce","code_verifier":"verifier"}`))
		case "/api/v1/auth/oidc/callback":
			close(exchangeStarted)
			<-releaseExchange
			_, _ = writer.Write([]byte(`{"accessToken":"access","refreshToken":"refresh","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(upstream.Close)
	manager, err := New(t.TempDir())
	require.NoError(t, err)
	start, err := manager.BeginPingCAPAuth(context.Background(), upstream.URL, false, "")
	require.NoError(t, err)
	manager.oidcMu.Lock()
	callbackURL := manager.oidcFlows[start.FlowID].redirectURI + "?code=code&state=state"
	manager.oidcMu.Unlock()

	callbackDone := make(chan struct{})
	go func() {
		defer close(callbackDone)
		response, requestErr := http.Get(callbackURL)
		if requestErr == nil {
			_ = response.Body.Close()
		}
	}()
	<-exchangeStarted
	manager.CancelPingCAPAuth(start.FlowID)
	close(releaseExchange)
	<-callbackDone

	require.Empty(t, manager.WorkspaceChoices(context.Background()))
	manager.mu.RLock()
	require.Empty(t, manager.servers)
	require.Empty(t, manager.runtimes)
	manager.mu.RUnlock()
	_, statErr := os.Stat(filepath.Join(manager.configDir, authFileName))
	require.True(t, errors.Is(statErr, os.ErrNotExist))
}

func TestCancelPingCAPAuthWinsBeforeCredentialCommit(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	persistEntered := make(chan struct{})
	releasePersist := make(chan struct{})
	upstream := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/public/config":
			_, _ = writer.Write([]byte(`{"pingcapAuthEnabled":true}`))
		case "/api/v1/auth/oidc/authorize-url":
			_, _ = writer.Write([]byte(`{"authorization_url":"https://auth.example.test/login","state":"state","nonce":"nonce","code_verifier":"verifier"}`))
		case "/api/v1/auth/oidc/callback":
			_, _ = writer.Write([]byte(`{"accessToken":"access","refreshToken":"refresh","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(upstream.Close)
	manager, err := New(t.TempDir())
	require.NoError(t, err)
	manager.oidcPersistHook = func() {
		close(persistEntered)
		<-releasePersist
	}
	start, err := manager.BeginPingCAPAuth(context.Background(), upstream.URL, false, "")
	require.NoError(t, err)
	manager.oidcMu.Lock()
	callbackURL := manager.oidcFlows[start.FlowID].redirectURI + "?code=code&state=state"
	manager.oidcMu.Unlock()

	callbackDone := make(chan struct{})
	go func() {
		defer close(callbackDone)
		response, requestErr := http.Get(callbackURL)
		if requestErr == nil {
			_ = response.Body.Close()
		}
	}()
	<-persistEntered
	manager.CancelPingCAPAuth(start.FlowID)
	close(releasePersist)
	<-callbackDone

	require.Empty(t, manager.WorkspaceChoices(context.Background()))
	_, statErr := os.Stat(filepath.Join(manager.configDir, authFileName))
	require.True(t, errors.Is(statErr, os.ErrNotExist))
}

func setUserAuthTestEncryptionKeys(t *testing.T) {
	t.Helper()
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	require.NoError(t, err)
	require.NoError(t, crypto.SetKeys(publicKey, privateKey))
	t.Cleanup(crypto.ResetKeys)
}

func TestLoginEncryptsRefreshTokenAtRest(t *testing.T) {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	require.NoError(t, err)
	require.NoError(t, crypto.SetKeys(publicKey, privateKey))
	t.Cleanup(crypto.ResetKeys)

	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/api/v1/auth/login":
			writer.Header().Set("Content-Type", "application/json")
			_, _ = writer.Write([]byte(`{"accessToken":"access-plain","refreshToken":"refresh-plain","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
		case "/api/v1/workspaces":
			require.Equal(t, "Bearer access-plain", request.Header.Get("Authorization"))
			writer.Header().Set("Content-Type", "application/json")
			_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(server.Close)

	configDir := t.TempDir()
	manager, err := New(configDir)
	require.NoError(t, err)
	result, err := manager.Login(context.Background(), server.URL, "user@example.com", "password")
	require.NoError(t, err)
	require.Len(t, result.Workspaces, 1)

	authPath := filepath.Join(configDir, authFileName)
	data, err := os.ReadFile(authPath)
	require.NoError(t, err)
	require.NotContains(t, string(data), "access-plain")
	require.NotContains(t, string(data), "refresh-plain")
	require.NotContains(t, string(data), "password")
	info, err := os.Stat(authPath)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
}

func TestResumeRefreshesExpiredAccessTokenAndRetriesWorkspaceRequest(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	var workspaceCalls atomic.Int32
	var refreshCalls atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/auth/login":
			_, _ = writer.Write([]byte(`{"accessToken":"stale-access","refreshToken":"refresh-one","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
		case "/api/v1/auth/refresh":
			refreshCalls.Add(1)
			var body map[string]string
			require.NoError(t, json.NewDecoder(request.Body).Decode(&body))
			require.Equal(t, "refresh-one", body["refreshToken"])
			_, _ = writer.Write([]byte(`{"accessToken":"fresh-access","refreshToken":"refresh-two"}`))
		case "/api/v1/workspaces":
			call := workspaceCalls.Add(1)
			if call == 1 {
				require.Equal(t, "Bearer stale-access", request.Header.Get("Authorization"))
				_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
				return
			}
			if request.Header.Get("Authorization") == "Bearer stale-access" {
				writer.WriteHeader(http.StatusUnauthorized)
				_, _ = writer.Write([]byte(`{"error":"access token expired"}`))
				return
			}
			require.Equal(t, "Bearer fresh-access", request.Header.Get("Authorization"))
			_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(server.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	result, err := manager.Login(context.Background(), server.URL, "user@example.com", "password")
	require.NoError(t, err)

	resumed, err := manager.Resume(context.Background(), result.Server.ID)
	require.NoError(t, err)
	require.Len(t, resumed.Workspaces, 1)
	require.Equal(t, int32(1), refreshCalls.Load())
	require.Equal(t, int32(3), workspaceCalls.Load())
	token, err := manager.UserToken(context.Background(), result.Server.ID)
	require.NoError(t, err)
	require.Equal(t, "fresh-access", token)

	data, err := os.ReadFile(filepath.Join(manager.configDir, authFileName))
	require.NoError(t, err)
	require.NotContains(t, string(data), "refresh-one")
	require.NotContains(t, string(data), "refresh-two")
}

func TestHTTPLoginPersistsAndResumesWithRefresh(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	var refreshCalls atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/auth/login":
			_, _ = writer.Write([]byte(`{"accessToken":"initial-access","refreshToken":"refresh-one","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
		case "/api/v1/auth/refresh":
			refreshCalls.Add(1)
			_, _ = writer.Write([]byte(`{"accessToken":"resumed-access","refreshToken":"refresh-two"}`))
		case "/api/v1/workspaces":
			require.Contains(t, []string{"Bearer initial-access", "Bearer resumed-access"}, request.Header.Get("Authorization"))
			_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(server.Close)

	configDir := t.TempDir()
	manager, err := New(configDir)
	require.NoError(t, err)
	login, err := manager.Login(context.Background(), server.URL, "user@example.com", "password")
	require.NoError(t, err)
	require.Equal(t, server.URL, login.Server.ServerURL)

	reloaded, err := New(configDir)
	require.NoError(t, err)
	resumed, err := reloaded.Resume(context.Background(), login.Server.ID)
	require.NoError(t, err)
	require.Equal(t, server.URL, resumed.Server.ServerURL)
	require.Len(t, resumed.Workspaces, 1)
	require.Equal(t, int32(1), refreshCalls.Load())
}

func TestWorkspaceChoicesClassifiesSavedLoginFailures(t *testing.T) {
	for _, test := range []struct {
		name     string
		status   int
		expected string
	}{
		{
			name:     "refresh token rejected",
			status:   http.StatusUnauthorized,
			expected: SavedLoginErrorReauthenticationRequired,
		},
		{
			name:     "rate limited",
			status:   http.StatusTooManyRequests,
			expected: SavedLoginErrorTemporarilyUnavailable,
		},
		{
			name:     "server unavailable",
			status:   http.StatusServiceUnavailable,
			expected: SavedLoginErrorTemporarilyUnavailable,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			setUserAuthTestEncryptionKeys(t)
			var refreshStatus atomic.Int32
			refreshStatus.Store(http.StatusOK)
			server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
				writer.Header().Set("Content-Type", "application/json")
				switch request.URL.Path {
				case "/api/v1/auth/login":
					_, _ = writer.Write([]byte(`{"accessToken":"access","refreshToken":"refresh","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
				case "/api/v1/auth/refresh":
					status := int(refreshStatus.Load())
					if status != http.StatusOK {
						writer.WriteHeader(status)
						_, _ = writer.Write([]byte(`{"error":"refresh failed"}`))
						return
					}
					_, _ = writer.Write([]byte(`{"accessToken":"fresh","refreshToken":"rotated"}`))
				case "/api/v1/workspaces":
					_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
				default:
					http.NotFound(writer, request)
				}
			}))
			t.Cleanup(server.Close)

			configDir := t.TempDir()
			manager, err := New(configDir)
			require.NoError(t, err)
			_, err = manager.Login(context.Background(), server.URL, "user@example.com", "password")
			require.NoError(t, err)
			refreshStatus.Store(int32(test.status))

			reloaded, err := New(configDir)
			require.NoError(t, err)
			choices := reloaded.WorkspaceChoices(context.Background())
			require.Len(t, choices, 1)
			require.Equal(t, test.expected, choices[0].ErrorCode)
			require.NotEmpty(t, choices[0].Error)
			require.True(t, choices[0].Server.Saved)
		})
	}
}

func TestLogoutRevokesBestEffortAndRemovesSavedSession(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	var logoutCalls atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/auth/login":
			_, _ = writer.Write([]byte(`{"accessToken":"access-plain","refreshToken":"refresh-plain","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
		case "/api/v1/workspaces":
			_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
		case "/api/v1/auth/logout":
			logoutCalls.Add(1)
			require.Equal(t, "Bearer access-plain", request.Header.Get("Authorization"))
			var body map[string]string
			require.NoError(t, json.NewDecoder(request.Body).Decode(&body))
			require.Equal(t, "refresh-plain", body["refreshToken"])
			writer.WriteHeader(http.StatusBadGateway)
			_, _ = writer.Write([]byte(`{"error":"offline"}`))
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(server.Close)

	configDir := t.TempDir()
	manager, err := New(configDir)
	require.NoError(t, err)
	result, err := manager.Login(context.Background(), server.URL, "user@example.com", "password")
	require.NoError(t, err)
	require.NoError(t, manager.Logout(context.Background(), result.Server.ID))
	require.Equal(t, int32(1), logoutCalls.Load())
	_, err = manager.Resume(context.Background(), result.Server.ID)
	require.EqualError(t, err, "saved server login not found")

	reloaded, err := New(configDir)
	require.NoError(t, err)
	require.Empty(t, reloaded.WorkspaceChoices(context.Background()))
}

func TestLogoutSaveFailureDoesNotRevokeOrClearRuntime(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	var logoutCalls atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/auth/login":
			_, _ = writer.Write([]byte(`{"accessToken":"access-plain","refreshToken":"refresh-plain","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
		case "/api/v1/workspaces":
			require.Equal(t, "Bearer access-plain", request.Header.Get("Authorization"))
			_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
		case "/api/v1/auth/logout":
			logoutCalls.Add(1)
			writer.WriteHeader(http.StatusNoContent)
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(server.Close)
	manager, err := New(t.TempDir())
	require.NoError(t, err)
	result, err := manager.Login(context.Background(), server.URL, "user@example.com", "password")
	require.NoError(t, err)
	manager.saveHook = func() error { return errors.New("disk full") }

	err = manager.Logout(context.Background(), result.Server.ID)
	require.ErrorContains(t, err, "disk full")
	require.Zero(t, logoutCalls.Load())
	token, err := manager.UserToken(context.Background(), result.Server.ID)
	require.NoError(t, err)
	require.Equal(t, "access-plain", token)
	resumed, err := manager.Resume(context.Background(), result.Server.ID)
	require.NoError(t, err)
	require.Len(t, resumed.Workspaces, 1)
}

func TestTargetSeparatesWorkspacesOnOneServer(t *testing.T) {
	manager := &Manager{
		servers: map[string]persistedServer{
			"uas_1": {
				ID:        "uas_1",
				Name:      "AgentTeam",
				ServerURL: "https://agentteam.example.com",
			},
		},
		runtimes: make(map[string]*runtimeSession),
	}

	firstID := BuildTargetID("uas_1", "wsp_one")
	secondID := BuildTargetID("uas_1", "wsp_two")
	require.NotEqual(t, firstID, secondID)
	first, ok := manager.Target(firstID)
	require.True(t, ok)
	second, ok := manager.Target(secondID)
	require.True(t, ok)
	require.Equal(t, "wsp_one", first.WorkspaceID)
	require.Equal(t, "wsp_two", second.WorkspaceID)
}

func TestLastActiveTargetPersistsWithoutCredentials(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	configDir := t.TempDir()
	encryptedRefreshToken, err := crypto.Encrypt("refresh-secret")
	require.NoError(t, err)
	manager := &Manager{
		configDir: configDir,
		servers: map[string]persistedServer{
			"uas_1": {
				ID:                    "uas_1",
				Name:                  "AgentTeam",
				ServerURL:             "https://agentteam.example.com",
				EncryptedRefreshToken: encryptedRefreshToken,
			},
		},
		runtimes: make(map[string]*runtimeSession),
	}
	targetID := BuildTargetID("uas_1", "wsp_one")
	require.NoError(t, manager.SetLastActiveTarget(targetID))

	reloaded, err := New(configDir)
	require.NoError(t, err)
	got, ok := reloaded.LastActiveTarget()
	require.True(t, ok)
	require.Equal(t, targetID, got)

	data, err := os.ReadFile(filepath.Join(configDir, authFileName))
	require.NoError(t, err)
	require.Contains(t, string(data), "last_active_target:")
	require.NotContains(t, string(data), "accessToken")
	require.NotContains(t, string(data), "refresh-secret")
}

func TestLogoutClearsLastActiveTargetForRemovedAccount(t *testing.T) {
	setUserAuthTestEncryptionKeys(t)
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		switch request.URL.Path {
		case "/api/v1/auth/login":
			_, _ = writer.Write([]byte(`{"accessToken":"access","refreshToken":"refresh","user":{"id":"usr_1","email":"user@example.com","name":"User"}}`))
		case "/api/v1/workspaces":
			_, _ = writer.Write([]byte(`[{"id":"wsp_1","name":"Workspace","slug":"workspace"}]`))
		case "/api/v1/auth/logout":
			writer.WriteHeader(http.StatusNoContent)
		default:
			http.NotFound(writer, request)
		}
	}))
	t.Cleanup(server.Close)

	manager, err := New(t.TempDir())
	require.NoError(t, err)
	result, err := manager.Login(context.Background(), server.URL, "user@example.com", "password")
	require.NoError(t, err)
	require.NoError(t, manager.SetLastActiveTarget(BuildTargetID(result.Server.ID, "wsp_1")))
	require.NoError(t, manager.Logout(context.Background(), result.Server.ID))
	_, ok := manager.LastActiveTarget()
	require.False(t, ok)
}

func TestSummariesIncludesConfiguredMachineServerAsLoginChoice(t *testing.T) {
	manager := &Manager{
		servers:  make(map[string]persistedServer),
		runtimes: make(map[string]*runtimeSession),
	}

	summaries := manager.Summaries([]profile.AgentTeamServer{{
		ID:        "srv_1",
		Enabled:   true,
		Name:      "Private AgentTeam",
		ServerURL: "https://agentteam.example.com/",
	}})

	require.Len(t, summaries, 2)
	require.Equal(t, DefaultServerURL(), summaries[0].ServerURL)
	require.Equal(t, "Private AgentTeam", summaries[1].Name)
	require.Equal(t, "https://agentteam.example.com", summaries[1].ServerURL)
	require.False(t, summaries[1].Saved)
}

func TestNormalizeServerURLAllowsHTTPAndHTTPSOrigins(t *testing.T) {
	for _, testCase := range []struct {
		name string
		raw  string
		want string
	}{
		{name: "HTTP domain", raw: "http://agentteam.example.com", want: "http://agentteam.example.com"},
		{name: "HTTP public IP and port", raw: "http://203.0.113.8:3001/", want: "http://203.0.113.8:3001"},
		{name: "HTTPS domain", raw: "https://agentteam.example.com/", want: "https://agentteam.example.com"},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			got, err := normalizeServerURL(testCase.raw)
			require.NoError(t, err)
			require.Equal(t, testCase.want, got)
		})
	}
}

func TestHTTPClientRejectsRedirectWithUserinfo(t *testing.T) {
	client, err := newHTTPClient("http://agentteam.example.com:3001")
	require.NoError(t, err)

	request, err := http.NewRequest(http.MethodGet, "http://user@agentteam.example.com:3001/next", nil)
	require.NoError(t, err)
	require.ErrorIs(t, client.CheckRedirect(request, nil), errCrossOriginRedirect)
}
