package knowledgeqa

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/forwarder"
	"github.com/tidbcloud/AgentTeam/internal/knowledgeqa/protocol"
	"github.com/tidbcloud/AgentTeam/internal/maas/availability"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type fakeSessionStore struct {
	createSessionParams CreateSessionParams
	createTurnParams    CreateTurnParams
	retryParams         RetryTurnParams
	stopTurnID          string
	createSession       Session
	createTurn          Turn
	createTurnErr       error
	retryTurn           Turn
	retryErr            error
	stopTurn            Turn
	stopErr             error
	loadedSession       Session
	turns               []Turn
	deleteSession       Session
	deleteErr           error
	citation            Citation
	citationErr         error
	feedbackSnapshot    FeedbackSnapshot
	feedbackSnapshotErr error
}

func (store *fakeSessionStore) CreateSession(_ context.Context, params CreateSessionParams) (Session, error) {
	store.createSessionParams = params
	return store.createSession, nil
}

func (store *fakeSessionStore) LoadSessionForOwner(context.Context, string, string, string) (Session, error) {
	return store.loadedSession, nil
}

func (store *fakeSessionStore) ListTurnsForOwner(context.Context, string, string, string, int) ([]Turn, error) {
	return store.turns, nil
}

func (store *fakeSessionStore) LoadCitationForOwner(context.Context, string, string, string, string) (Citation, error) {
	return store.citation, store.citationErr
}

func (store *fakeSessionStore) LoadFeedbackSnapshotForOwner(context.Context, string, string, string, string) (FeedbackSnapshot, error) {
	return store.feedbackSnapshot, store.feedbackSnapshotErr
}

func (store *fakeSessionStore) CreateTurn(_ context.Context, params CreateTurnParams) (Turn, error) {
	store.createTurnParams = params
	return store.createTurn, store.createTurnErr
}

func (store *fakeSessionStore) RetryTurn(_ context.Context, params RetryTurnParams) (Turn, error) {
	store.retryParams = params
	return store.retryTurn, store.retryErr
}

func (store *fakeSessionStore) StopTurnForOwner(_ context.Context, _, _, turnID string, _ time.Time) (Turn, error) {
	store.stopTurnID = turnID
	return store.stopTurn, store.stopErr
}

func (store *fakeSessionStore) DeleteSessionConversationForOwner(context.Context, string, string, string, time.Time) (Session, error) {
	return store.deleteSession, store.deleteErr
}

type fakePolicyResolver struct {
	organization protocol.OrganizationPolicy
	effective    protocol.EffectivePolicy
	err          error
}

func (resolver fakePolicyResolver) LoadOrganizationPolicy(context.Context, string) (protocol.OrganizationPolicy, error) {
	return resolver.organization, resolver.err
}

func (resolver fakePolicyResolver) ResolveEffectivePolicy(context.Context, string, string, string, string) (protocol.EffectivePolicy, error) {
	return resolver.effective, resolver.err
}

type fakeOrganizationResolver struct {
	summary Organization
	err     error
}

type fakeHostedAvailabilityResolver struct {
	availability availability.Availability
	err          error
}

func (resolver fakeHostedAvailabilityResolver) LoadHostedAvailability(context.Context, string) (availability.Availability, error) {
	return resolver.availability, resolver.err
}

func (resolver fakeOrganizationResolver) GetWorkspaceOrganizationForUser(context.Context, string, string) (Organization, error) {
	return resolver.summary, resolver.err
}

type fakeQueue struct {
	mu              sync.Mutex
	queuedTurnID    string
	queuedWorkspace string
	queuedAttempt   string
	canceledTurnID  string
}

type fakeTurnUpdates struct {
	updates []protocol.TurnUpdate
}

func (updates *fakeTurnUpdates) NotifyKnowledgeQATurnUpdated(_ context.Context, update protocol.TurnUpdate) {
	updates.updates = append(updates.updates, update)
}

type fakeCitationOpener struct {
	request forwarder.KnowledgeQACitationOpenRequest
	result  forwarder.KnowledgeQACitationOpenResult
}

func (opener *fakeCitationOpener) OpenKnowledgeQACitation(_ context.Context, request forwarder.KnowledgeQACitationOpenRequest) (forwarder.KnowledgeQACitationOpenResult, error) {
	opener.request = request
	return opener.result, nil
}

func (queue *fakeQueue) NotifyKnowledgeQATurnQueued(_ context.Context, workspaceID, turnID, attemptID string) {
	queue.mu.Lock()
	defer queue.mu.Unlock()
	queue.queuedWorkspace = workspaceID
	queue.queuedTurnID = turnID
	queue.queuedAttempt = attemptID
}

func (queue *fakeQueue) CancelKnowledgeQATurn(_ context.Context, turnID, _ string) {
	queue.mu.Lock()
	defer queue.mu.Unlock()
	queue.canceledTurnID = turnID
}

type fakeFeedbackService struct {
	upsertParams UpsertFeedbackParams
	feedback     Feedback
	feedbacks    []Feedback
	deletedTurn  string
	err          error
}

type fakeSharingService struct {
	createWorkspaceID string
	createOwnerUserID string
	createSessionID   string
	createTurnID      string
	openedUserID      string
	openedToken       string
	loadedUserID      string
	loadedShareID     string
	loadedEvidenceID  string
	listedUserID      string
	listedLimit       int
	share             Share
	shareCitation     ShareCitation
	shares            []Share
	err               error
}

func (service *fakeSharingService) CreateShare(_ context.Context, workspaceID, ownerUserID, sessionID, turnID string, _ time.Time) (Share, error) {
	service.createWorkspaceID = workspaceID
	service.createOwnerUserID = ownerUserID
	service.createSessionID = sessionID
	service.createTurnID = turnID
	return service.share, service.err
}

func (service *fakeSharingService) OpenShare(_ context.Context, userID, token string, _ time.Time) (Share, error) {
	service.openedUserID = userID
	service.openedToken = token
	return service.share, service.err
}

func (service *fakeSharingService) LoadReceivedShare(_ context.Context, userID, shareID string, _ time.Time) (Share, error) {
	service.loadedUserID = userID
	service.loadedShareID = shareID
	return service.share, service.err
}

func (service *fakeSharingService) LoadReceivedShareCitation(_ context.Context, userID, shareID, evidenceID string) (ShareCitation, error) {
	service.loadedUserID = userID
	service.loadedShareID = shareID
	service.loadedEvidenceID = evidenceID
	return service.shareCitation, service.err
}

func (service *fakeSharingService) ListReceivedShares(_ context.Context, userID string, limit int) ([]Share, error) {
	service.listedUserID = userID
	service.listedLimit = limit
	return service.shares, service.err
}

func (service *fakeFeedbackService) UpsertFeedback(_ context.Context, params UpsertFeedbackParams) (Feedback, error) {
	service.upsertParams = params
	return service.feedback, service.err
}

func (service *fakeFeedbackService) ListFeedbackForThread(context.Context, string, string, string) ([]Feedback, error) {
	return service.feedbacks, service.err
}

func (service *fakeFeedbackService) DeleteFeedback(_ context.Context, _, _, turnID string) error {
	service.deletedTurn = turnID
	return service.err
}

func availablePolicy() fakePolicyResolver {
	return fakePolicyResolver{
		organization: protocol.OrganizationPolicy{
			OrganizationID: "org_1", Enabled: true, PolicyVersion: 4,
		},
		effective: protocol.EffectivePolicy{
			Model:                     "model-a",
			ReasonLevel:               "high",
			OrganizationPolicyVersion: 4,
			WorkloadKind:              "knowledge_qa",
		},
	}
}

func availableHostedModel() fakeHostedAvailabilityResolver {
	return fakeHostedAvailabilityResolver{availability: availability.Availability{
		EntitlementStatus: availability.EntitlementStatusEligible,
		BalanceStatus:     availability.BalanceStatusPositive,
		Models: []availability.Model{
			{
				ModelReference:            "model-a",
				DisplayName:               "Model A",
				Enabled:                   true,
				InventoryStatus:           availability.InventoryStatusActive,
				EntitlementStatus:         availability.EntitlementStatusEligible,
				BalanceStatus:             availability.BalanceStatusPositive,
				RuntimeStatus:             availability.RuntimeStatusSupported,
				ExecutableWireAPIFamilies: []string{"chat_completions"},
				ChatCompletionsFeatures: []string{
					"chat_completions.reasoning_effort",
					"chat_completions.tools",
					"chat_completions.tool_choice",
					"chat_completions.parallel_tool_calls",
				},
				ChatCompletionsCompatibilityProfile: openAIKnowledgeQAProfile(),
			},
			{
				ModelReference:  "model-b",
				DisplayName:     "Model B",
				Enabled:         false,
				InventoryStatus: availability.InventoryStatusActive,
			},
		},
	}}
}

func openAIKnowledgeQAProfile() *availability.ChatCompletionsCompatibilityProfile {
	return &availability.ChatCompletionsCompatibilityProfile{
		ID:                                 availability.ChatCompletionsProfileOpenAIRequiredToolsV1,
		SupportedToolChoiceModes:           []string{"none", "auto", "required", "named"},
		ToolChoiceParameterAccepted:        true,
		ParallelToolCallsParameterAccepted: true,
		AssistantToolCallReasoningReplay:   "optional",
		AssistantToolCallContent:           "nullable",
		OutputTokenField:                   "max_completion_tokens",
		SupportsStoreFalse:                 true,
		SupportsNOne:                       true,
		ReasoningEffortMap: map[string]string{
			"low": "low", "medium": "medium", "high": "high",
		},
	}
}

func deepSeekKnowledgeQAProfile() *availability.ChatCompletionsCompatibilityProfile {
	return &availability.ChatCompletionsCompatibilityProfile{
		ID:                                 availability.ChatCompletionsProfileDeepSeekV4ThinkingV1,
		SupportedToolChoiceModes:           []string{"auto"},
		ToolChoiceParameterAccepted:        false,
		ParallelToolCallsParameterAccepted: false,
		AssistantToolCallReasoningReplay:   "required",
		AssistantToolCallContent:           "non_null_string",
		OutputTokenField:                   "max_tokens",
		SupportsStoreFalse:                 false,
		SupportsNOne:                       false,
		ReasoningEffortMap: map[string]string{
			"low": "high", "medium": "high", "high": "high",
			"xhigh": "max", "max": "max",
		},
	}
}

func TestCreateSessionQueuesServerHostedTurnWithoutAgentOrMachineSelection(t *testing.T) {
	session := Session{
		ID: "kqs_12345678", WorkspaceID: "wsp_1", OrganizationID: "org_1",
		OwnerUserID: "usr_1", OwnerMemberID: "mem_1",
		RootMessageID: "msg_1", ThreadChannelID: "thr_1",
		RequestedKnowledgeBaseIDs: []string{"kb_1"},
		RequestedSourceIDs:        []string{"src_1"},
		RequestedModel:            "model-a",
		RequestedReasonLevel:      "high",
		WebSearchEnabled:          true,
		Status:                    "active",
	}
	turn := Turn{
		ID: "kqt_1", SessionID: session.ID, UserMessageID: "msg_1",
		Question: "Explain", Status: "queued", CurrentAttemptID: "kqa_1",
		RequestedModel: "model-a", RequestedReasonLevel: "high",
		WebSearchEnabled: true,
	}
	sessions := &fakeSessionStore{createSession: session, createTurn: turn}
	queue := &fakeQueue{}
	turnUpdates := &fakeTurnUpdates{}
	handler := newSessionHandler(Dependencies{
		Sessions: sessions,
		Policies: availablePolicy(),
		Organizations: fakeOrganizationResolver{
			summary: Organization{ID: "org_1"},
		},
		Queue: queue, TurnUpdates: turnUpdates,
		HostedAvailability: availableHostedModel(),
	})
	request := knowledgeQAWorkspaceJSON(dto.KnowledgeQACreateSessionRequest{
		SessionID: "kqs_12345678", RootMessageID: "msg_1",
		ThreadChannelID: "thr_1", UserMessageID: "msg_1",
		Question: "Explain", KnowledgeBaseIDs: []string{"kb_1"},
		SourceIDs: []string{"src_1"}, Model: "model-a", ReasonLevel: "high",
		WebSearchEnabled: true,
	}, "session:kqs_12345678")

	response, err := handler.CreateSession(request)
	require.NoError(t, err)
	require.Equal(t, http.StatusAccepted, response.Status)
	require.Equal(t, "org_1", sessions.createSessionParams.OrganizationID)
	require.Equal(t, []string{"kb_1"}, sessions.createSessionParams.RequestedKnowledgeBaseIDs)
	require.Equal(t, []string{"src_1"}, sessions.createSessionParams.RequestedSourceIDs)
	require.Equal(t, "model-a", sessions.createSessionParams.RequestedModel)
	require.Equal(t, "high", sessions.createSessionParams.RequestedReasonLevel)
	require.Equal(t, true, sessions.createSessionParams.WebSearchEnabled)
	require.Equal(t, "session:kqs_12345678", sessions.createTurnParams.IdempotencyKey)
	require.Equal(t, "org_1", sessions.createTurnParams.OrganizationID)
	require.Equal(t, "kqt_1", queue.queuedTurnID)
	require.Equal(t, "kqa_1", queue.queuedAttempt)
	require.Equal(t, []protocol.TurnUpdate{{
		WorkspaceID: "wsp_1", OwnerMemberID: "mem_1",
		ThreadChannelID: "thr_1", TurnID: "kqt_1",
	}}, turnUpdates.updates)

	payload, err := json.Marshal(response.Body)
	require.NoError(t, err)
	require.JSONEq(t, `{
	  "session": {
	    "id":"kqs_12345678",
	    "threadChannelId":"thr_1",
	    "rootMessageId":"msg_1",
	    "runtime":"server_hosted",
	    "managedBy":"organization",
	    "model":"model-a",
	    "reasonLevel":"high",
	    "webSearchEnabled":true,
	    "knowledgeBaseIds":["kb_1"],
	    "sourceIds":["src_1"],
	    "status":"active"
	  },
	  "turn": {
	    "id":"kqt_1",
	    "sessionId":"kqs_12345678",
	    "userMessageId":"msg_1",
	    "assistantMessageId":"",
	    "question":"Explain",
	    "model":"model-a",
	    "reasonLevel":"high",
	    "webSearchEnabled":true,
	    "status":"queued",
	    "activities":[]
	  }
	}`, string(payload))
	require.NotContains(t, string(payload), "machine")
	require.NotContains(t, string(payload), "agent")
	require.Contains(t, string(payload), `"model":"model-a"`)
	require.Contains(t, string(payload), `"reasonLevel":"high"`)
}

func TestCreateTurnReturnsAtomicQueueLimitWithRetryAfter(t *testing.T) {
	session := Session{
		ID: "kqs_12345678", WorkspaceID: "wsp_1", OrganizationID: "org_1",
		OwnerUserID: "usr_1", Status: "active",
		RequestedModel: "model-a", RequestedReasonLevel: "high",
	}
	sessions := &fakeSessionStore{
		loadedSession: session,
		createTurnErr: ErrQueueFull,
	}
	handler := newTurnHandler(Dependencies{
		Sessions: sessions,
		Policies: availablePolicy(),
		Organizations: fakeOrganizationResolver{
			summary: Organization{ID: "org_1"},
		},
		HostedAvailability: availableHostedModel(),
		Queue:              &fakeQueue{},
	})
	request := knowledgeQAWorkspaceJSON(dto.KnowledgeQACreateTurnRequest{
		UserMessageID: "msg_2", Question: "Next question",
	}, "turn:msg_2")
	request.Gin.Params = gin.Params{{Key: "sessionId", Value: session.ID}}

	response, err := handler.CreateTurn(request)
	require.NoError(t, err)
	require.Equal(t, http.StatusTooManyRequests, response.Status)
	require.Equal(t, "10", response.Headers.Get("Retry-After"))
	require.Equal(t, gin.H{
		"error":   "knowledge_qa_queue_full",
		"message": "Knowledge Q&A queue is full",
	}, response.Body)
}

func TestPolicyStatusReportsHostedAdmissionBlockersWithModelCatalog(t *testing.T) {
	tests := []struct {
		name         string
		availability availability.Availability
		status       string
	}{
		{
			name: "credits",
			availability: availability.Availability{
				EntitlementStatus: availability.EntitlementStatusEligible,
				BalanceStatus:     "zero_balance",
			},
			status: "credits_insufficient",
		},
		{
			name: "entitlement",
			availability: availability.Availability{
				EntitlementStatus: "not_entitled",
				BalanceStatus:     availability.BalanceStatusPositive,
			},
			status: "entitlement_invalid",
		},
		{
			name: "model unavailable",
			availability: availability.Availability{
				EntitlementStatus: availability.EntitlementStatusEligible,
				BalanceStatus:     availability.BalanceStatusPositive,
				Models:            []availability.Model{{ModelReference: "other-model"}},
			},
			status: "model_unavailable",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			handler := newPolicyHandler(Dependencies{
				Policies:      availablePolicy(),
				Organizations: fakeOrganizationResolver{summary: Organization{ID: "org_1"}},
				HostedAvailability: fakeHostedAvailabilityResolver{
					availability: test.availability,
				},
				Queue: &fakeQueue{},
			})
			response, err := handler.GetPolicyStatus(knowledgeQAWorkspaceRequest())
			require.NoError(t, err)
			payload, err := json.Marshal(response.Body)
			require.NoError(t, err)
			require.Contains(t, string(payload), `"status":"`+test.status+`"`)
			require.Contains(t, string(payload), `"reasonLevels":[]`)
		})
	}
}

func TestRetryCreatesNewAttemptAndStopCancelsCurrentAttempt(t *testing.T) {
	session := Session{
		ID: "kqs_12345678", WorkspaceID: "wsp_1", OrganizationID: "org_1",
		OwnerUserID: "usr_1", Status: "active",
		RequestedModel: "model-a", RequestedReasonLevel: "high",
	}
	retry := Turn{
		ID: "kqt_1", SessionID: session.ID, Status: "queued",
		CurrentAttemptID: "kqa_2",
	}
	stopped := retry
	stopped.Status = "canceled"
	sessions := &fakeSessionStore{
		loadedSession: session, retryTurn: retry, stopTurn: stopped,
	}
	queue := &fakeQueue{}
	handler := newTurnHandler(Dependencies{
		Sessions: sessions,
		Policies: availablePolicy(),
		Organizations: fakeOrganizationResolver{
			summary: Organization{ID: "org_1"},
		},
		Queue: queue, HostedAvailability: availableHostedModel(),
	})
	retryRequest := knowledgeQAWorkspaceRequest()
	retryRequest.Gin.Request.Header.Set("Idempotency-Key", "retry:kqt_1:2")
	retryRequest.Gin.Params = gin.Params{
		{Key: "sessionId", Value: session.ID},
		{Key: "turnId", Value: "kqt_1"},
	}
	response, err := handler.RetryTurn(retryRequest)
	require.NoError(t, err)
	require.Equal(t, http.StatusAccepted, response.Status)
	require.Equal(t, "retry:kqt_1:2", sessions.retryParams.IdempotencyKey)
	require.Equal(t, "kqa_2", queue.queuedAttempt)

	stopRequest := knowledgeQAWorkspaceRequest()
	stopRequest.Gin.Request.Header.Set("Idempotency-Key", "stop:kqt_1")
	stopRequest.Gin.Params = retryRequest.Gin.Params
	response, err = handler.StopTurn(stopRequest)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	require.Equal(t, "kqt_1", sessions.stopTurnID)
	require.Equal(t, "kqt_1", queue.canceledTurnID)
}

func TestPolicyStatusExposesHostedModelsAndReasonLevels(t *testing.T) {
	handler := newPolicyHandler(Dependencies{
		Policies: availablePolicy(),
		Organizations: fakeOrganizationResolver{
			summary: Organization{ID: "org_1"},
		},
		HostedAvailability: availableHostedModel(),
		Queue:              &fakeQueue{},
	})
	response, err := handler.GetPolicyStatus(knowledgeQAWorkspaceRequest())
	require.NoError(t, err)
	payload, err := json.Marshal(response.Body)
	require.NoError(t, err)
	require.Contains(t, string(payload), `"enabled":true`)
	require.Contains(t, string(payload), `"modelReference":"model-a"`)
	require.Contains(t, string(payload), `"displayName":"Model A"`)
	require.Contains(t, string(payload), `"modelReference":"model-b"`)
	require.Contains(t, string(payload), `"displayName":"Model B"`)
	require.Contains(t, string(payload), `"modelReference":"model-b","displayName":"Model B","enabled":false`)
	require.Contains(t, string(payload), `"reasonLevels":["low","medium","high"],"defaultReasonLevel":"medium"`)
	require.Contains(t, string(payload), `"reasonLevels":["low","medium","high"]`)
}

func TestHostedModelExecutableRequiresNativeToolsAndKnownProfile(t *testing.T) {
	model := availableHostedModel().availability.Models[0]
	require.True(t, hostedModelExecutable(model))

	model.ChatCompletionsFeatures = []string{"chat_completions.reasoning_effort"}
	require.False(t, hostedModelExecutable(model))

	model.ChatCompletionsFeatures = []string{"chat_completions.tools"}
	require.False(t, hostedModelExecutable(model))

	model = availableHostedModel().availability.Models[0]
	model.ChatCompletionsFeatures = []string{
		"chat_completions.reasoning_effort",
		"chat_completions.tools",
		"chat_completions.parallel_tool_calls",
	}
	require.False(t, hostedModelExecutable(model))

	model = availableHostedModel().availability.Models[0]
	model.ChatCompletionsFeatures = []string{
		"chat_completions.reasoning_effort",
		"chat_completions.tools",
		"chat_completions.tool_choice",
	}
	require.False(t, hostedModelExecutable(model))

	model = availableHostedModel().availability.Models[0]
	model.ChatCompletionsCompatibilityProfile = nil
	require.False(t, hostedModelExecutable(model))

	model = availableHostedModel().availability.Models[0]
	model.ChatCompletionsCompatibilityProfile.ID = "chat_completions_future_v99"
	require.False(t, hostedModelExecutable(model))
}

func TestDeepSeekProfileExposesOnlyTruthfulHighReasoningAndServerEnforcesIt(t *testing.T) {
	availability := availableHostedModel().availability
	availability.Models = availability.Models[:1]
	availability.Models[0].ModelReference = "agentteam/deepseek-v4-pro"
	availability.Models[0].ChatCompletionsFeatures = []string{
		"chat_completions.reasoning_effort",
		"chat_completions.tools",
	}
	availability.Models[0].ChatCompletionsCompatibilityProfile = deepSeekKnowledgeQAProfile()

	options := hostedModelOptions(availability)
	require.Len(t, options, 1)
	require.True(t, options[0].Enabled)
	require.Equal(t, []string{"high"}, options[0].ReasonLevels)
	require.Equal(t, "high", options[0].DefaultReasonLevel)
	levels, defaultLevel := hostedReasonLevelPolicy(options)
	require.Equal(t, []string{"high"}, levels)
	require.Equal(t, "high", defaultLevel)

	require.Equal(t, "available", hostedPolicyStatus(availability, "agentteam/deepseek-v4-pro", "high"))
	require.Equal(t, "model_unavailable", hostedPolicyStatus(availability, "agentteam/deepseek-v4-pro", "low"))
	require.Equal(t, "model_unavailable", hostedPolicyStatus(availability, "agentteam/deepseek-v4-pro", "medium"))
}

func TestCreateSessionRequiresIdempotencyAndRejectsLegacyRuntimeFields(t *testing.T) {
	request := dto.KnowledgeQACreateSessionRequest{
		SessionID: "kqs_12345678", RootMessageID: "msg_1",
		ThreadChannelID: "thr_1", UserMessageID: "msg_1", Question: "Explain",
		Model: "model-a", ReasonLevel: "medium",
	}
	require.NoError(t, validateCreateSessionRequest(request))
	_, err := requireIdempotencyKey(knowledgeQAWorkspaceRequest().Gin)
	require.Error(t, err)

	encoded, err := json.Marshal(request)
	require.NoError(t, err)
	require.NotContains(t, string(encoded), "machineId")
	require.NotContains(t, string(encoded), "runtime")
	require.Contains(t, string(encoded), `"model":"model-a"`)
	require.Contains(t, string(encoded), `"reasonLevel":"medium"`)
}

func TestDisabledOrganizationPolicyBlocksSubmission(t *testing.T) {
	handler := newSessionHandler(Dependencies{
		Sessions: &fakeSessionStore{},
		Policies: fakePolicyResolver{err: protocol.ErrOrganizationDisabled},
		Organizations: fakeOrganizationResolver{
			summary: Organization{ID: "org_1"},
		},
		Queue: &fakeQueue{},
	})
	request := knowledgeQAWorkspaceJSON(dto.KnowledgeQACreateSessionRequest{
		SessionID: "kqs_12345678", RootMessageID: "msg_1",
		ThreadChannelID: "thr_1", UserMessageID: "msg_1", Question: "Explain",
		Model: "model-a", ReasonLevel: "high",
	}, "session:kqs_12345678")
	_, err := handler.CreateSession(request)
	require.Error(t, err)
	var httpErr api.HTTPError
	require.True(t, errors.As(err, &httpErr))
	require.Equal(t, http.StatusForbidden, httpErr.Status)
	require.Equal(t, "knowledge_qa_organization_disabled", httpErr.Code)
}

func knowledgeQAWorkspaceRequest() api.WorkspaceScopedRequest {
	ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
	ctx.Request = httptest.NewRequest(http.MethodGet, "/", nil)
	return api.WorkspaceScopedRequest{
		AuthenticatedRequest: api.AuthenticatedRequest{
			Gin:  ctx,
			User: identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}},
		},
		Workspace: workspace.Summary{
			WorkspaceID: "wsp_1", MemberID: "mem_1", MemberUserID: "usr_1",
		},
	}
}

func knowledgeQAWorkspaceJSON[T any](body T, idempotencyKey string) api.WorkspaceScopedJSON[T] {
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Request.Header.Set("Idempotency-Key", idempotencyKey)
	return api.WorkspaceScopedJSON[T]{WorkspaceScopedRequest: request, Body: body}
}

func TestKnowledgeQAShareCreationAndOpenKeepInvitationBehindAuthentication(t *testing.T) {
	now := time.Date(2026, 8, 14, 10, 0, 0, 0, time.UTC)
	sharing := &fakeSharingService{share: Share{
		ID: "kqh_1234567890abcd", OwnerDisplayName: "Owner",
		Question: "Question?", Answer: "Answer [E1].", Token: "kqshare_secret",
		CreatedAt: now,
	}}
	handler := newShareHandler(Dependencies{
		Sharing:       sharing,
		PublicBaseURL: "https://agentteam.example/app",
	})
	createRequest := knowledgeQAWorkspaceRequest()
	createRequest.Gin.Params = gin.Params{
		{Key: "sessionId", Value: "kqs_1"},
		{Key: "turnId", Value: "kqt_1"},
	}
	response, err := handler.Create(createRequest)
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, response.Status)
	require.Equal(t, "private, no-store", response.Headers.Get("Cache-Control"))
	require.Equal(t, "wsp_1", sharing.createWorkspaceID)
	require.Equal(t, "usr_1", sharing.createOwnerUserID)
	require.Equal(t, "kqs_1", sharing.createSessionID)
	require.Equal(t, "kqt_1", sharing.createTurnID)
	created := response.Body.(dto.KnowledgeQAShareCreateResponse)
	require.Equal(t, "kqh_1234567890abcd", created.ShareID)
	require.Equal(t, "https://agentteam.example/app/kb/shared#kqshare_secret", created.URL)
	createdPayload, err := json.Marshal(created)
	require.NoError(t, err)
	require.Equal(t, 1, strings.Count(string(createdPayload), "kqshare_secret"))
	require.NotContains(t, string(createdPayload), `"token"`)

	openResponse, err := handler.Open(api.WorkspaceScopedJSON[dto.KnowledgeQAShareOpenRequest]{
		WorkspaceScopedRequest: createRequest,
		Body:                   dto.KnowledgeQAShareOpenRequest{Token: "kqshare_secret"},
	})
	require.NoError(t, err)
	require.Equal(t, "usr_1", sharing.openedUserID)
	require.Equal(t, "kqshare_secret", sharing.openedToken)
	require.Equal(t, "private, no-store", openResponse.Headers.Get("Cache-Control"))
	payload, err := json.Marshal(openResponse.Body)
	require.NoError(t, err)
	require.NotContains(t, string(payload), "workspaceId")
	require.NotContains(t, string(payload), "sessionId")
	require.NotContains(t, string(payload), "token")
	require.NotContains(t, string(payload), "citation")

	sharing.shares = []Share{{
		ID: "kqh_1234567890abcd", OwnerDisplayName: "Owner",
		Question: "Question?", Answer: "answer must not enter the list", CreatedAt: now,
		LastOpenedAt: now,
	}}
	listResponse, err := handler.List(createRequest)
	require.NoError(t, err)
	require.Equal(t, "usr_1", sharing.listedUserID)
	require.Equal(t, 20, sharing.listedLimit)
	listPayload, err := json.Marshal(listResponse.Body)
	require.NoError(t, err)
	require.NotContains(t, string(listPayload), "answer must not enter the list")
	require.NotContains(t, string(listPayload), `"answer"`)
}

func TestKnowledgeQAShareRoutesExposeNoAnonymousReader(t *testing.T) {
	router := gin.New()
	sharing := &fakeSharingService{}
	RegisterRoutes(router, Dependencies{Sharing: sharing})
	routes := make(map[string]struct{})
	for _, route := range router.Routes() {
		routes[route.Method+" "+route.Path] = struct{}{}
	}
	for _, route := range []string{
		"POST /api/v1/workspaces/:workspaceId/knowledge-qa/sessions/:sessionId/turns/:turnId/share",
		"POST /api/v1/workspaces/:workspaceId/knowledge-qa/shares/open",
		"GET /api/v1/workspaces/:workspaceId/knowledge-qa/shares",
		"GET /api/v1/workspaces/:workspaceId/knowledge-qa/shares/:shareId/evidence/:evidenceId",
		"GET /api/v1/workspaces/:workspaceId/knowledge-qa/shares/:shareId",
	} {
		_, ok := routes[route]
		require.True(t, ok, "missing route %s", route)
	}
	for route := range routes {
		require.NotContains(t, route, "/public/")
	}

	for _, request := range []*http.Request{
		httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/knowledge-qa/sessions/kqs_1/turns/kqt_1/share", nil),
		httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/knowledge-qa/shares/open", strings.NewReader(`{"token":"kqshare_secret"}`)),
		httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/knowledge-qa/shares", nil),
		httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/knowledge-qa/shares/kqh_1234567890abcd/evidence/E1", nil),
		httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_1/knowledge-qa/shares/kqh_1234567890abcd", nil),
	} {
		request.Header.Set("Content-Type", "application/json")
		response := httptest.NewRecorder()
		router.ServeHTTP(response, request)
		require.Equal(t, http.StatusUnauthorized, response.Code, request.URL.Path)
	}
	require.Empty(t, sharing.openedToken)
	require.Empty(t, sharing.loadedShareID)
	require.Empty(t, sharing.createTurnID)
}

func TestReceivedShareEvidenceRechecksRecipientSourceAccessAndReturnsSidebarPayload(t *testing.T) {
	sharing := &fakeSharingService{shareCitation: ShareCitation{
		ShareID: "kqh_1234567890abcd", EvidenceID: "E1",
		SourceWorkspaceID: "wsp_source", SourceOrganizationID: "org_source",
		SourceTurnID: "kqt_source",
		Citation: Citation{
			CitationKind: "knowledge", KnowledgeBaseID: "kb_1", SourceID: "src_1",
			RootKind: "primary", RepoRole: "primary", Revision: "sha-historical",
			Path: "docs/design.md", LineStart: 4, LineEnd: 5,
		},
	}}
	opener := &fakeCitationOpener{result: forwarder.KnowledgeQACitationOpenResult{
		Status: "available", OpenMode: "source", Content: "line 4\nline 5\n",
		Path: "docs/design.md", Revision: "sha-historical", LineStart: 4, LineEnd: 5,
	}}
	handler := newShareHandler(Dependencies{Sharing: sharing, Citations: opener})
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Params = gin.Params{
		{Key: "shareId", Value: "kqh_1234567890abcd"},
		{Key: "evidenceId", Value: "E1"},
	}

	response, err := handler.OpenEvidence(request)
	require.NoError(t, err)
	require.Equal(t, "usr_1", sharing.loadedUserID)
	require.Equal(t, "kqh_1234567890abcd", sharing.loadedShareID)
	require.Equal(t, "E1", sharing.loadedEvidenceID)
	require.True(t, opener.request.AllowRestrictedEvidence)
	require.Equal(t, "wsp_source", opener.request.Principal.WorkspaceID)
	require.Equal(t, "org_source", opener.request.Principal.OrganizationID)
	require.Equal(t, "usr_1", opener.request.Principal.RequesterID)
	require.Equal(t, "kqt_source", opener.request.Principal.TurnID)

	payload := response.Body.(dto.KnowledgeQACitationOpenResponse)
	require.Equal(t, "source", payload.OpenMode)
	require.Equal(t, "wsp_source", payload.TargetWorkspaceID)
	require.Equal(t, "/kb/kb_1/sources/src_1/blob/sha-historical/docs/design.md#L4", payload.TargetRoute)
	require.NotNil(t, payload.Evidence)
	require.Equal(t, "line 4\nline 5\n", payload.Evidence.Content)
	require.Equal(t, "private, no-store", response.Headers.Get("Cache-Control"))
}

func TestReceivedShareEvidenceReturnsRestrictedPreviewWithoutRepositoryTarget(t *testing.T) {
	sharing := &fakeSharingService{shareCitation: ShareCitation{
		SourceWorkspaceID: "wsp_source", SourceOrganizationID: "org_source", SourceTurnID: "kqt_source",
		Citation: Citation{
			CitationKind: "knowledge", KnowledgeBaseID: "kb_1", SourceID: "src_1",
			RootKind: "primary", RepoRole: "primary", Revision: "sha-historical",
			Path: "docs/design.md", LineStart: 4, LineEnd: 5,
		},
	}}
	opener := &fakeCitationOpener{result: forwarder.KnowledgeQACitationOpenResult{
		Status: "available", OpenMode: "evidence", Content: "bounded\n",
		Path: "docs/design.md", Revision: "sha-historical", LineStart: 4, LineEnd: 5,
	}}
	handler := newShareHandler(Dependencies{Sharing: sharing, Citations: opener})
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Params = gin.Params{
		{Key: "shareId", Value: "kqh_1234567890abcd"},
		{Key: "evidenceId", Value: "E1"},
	}

	response, err := handler.OpenEvidence(request)
	require.NoError(t, err)
	payload := response.Body.(dto.KnowledgeQACitationOpenResponse)
	require.Equal(t, "evidence", payload.OpenMode)
	require.Empty(t, payload.TargetWorkspaceID)
	require.Empty(t, payload.TargetRoute)
	require.NotNil(t, payload.Evidence)
}

func TestKnowledgeQAFeedbackUsesOwnerAuthorizedSnapshotAndReturnsPersistedState(t *testing.T) {
	snapshot := FeedbackSnapshot{
		Turn: Turn{
			ID: "kqt_1", WorkspaceID: "wsp_1", OrganizationID: "org_1",
			SessionID: "kqs_1", Question: "Where is the setting?",
			Status: "completed", AssistantMessageID: "msg_answer",
		},
		ThreadChannelID: "thr_1",
		Answer:          "Use Settings.",
	}
	sessions := &fakeSessionStore{feedbackSnapshot: snapshot}
	feedbacks := &fakeFeedbackService{feedback: Feedback{
		ID: "kqf_1", TurnID: "kqt_1", Rating: "problem",
		Reasons: []string{"incorrect"}, Comment: "The setting moved",
		UpdatedAt: time.Date(2026, 8, 11, 12, 0, 0, 0, time.UTC),
	}}
	handler := newFeedbackHandler(Dependencies{Sessions: sessions, Feedbacks: feedbacks})
	request := knowledgeQAWorkspaceJSON(dto.KnowledgeQAFeedbackRequest{
		Rating: "problem", Reasons: []string{"incorrect"}, Comment: "The setting moved",
	}, "")
	request.Gin.Params = gin.Params{
		{Key: "sessionId", Value: "kqs_1"},
		{Key: "turnId", Value: "kqt_1"},
	}

	response, err := handler.Upsert(request)
	require.NoError(t, err)
	require.Equal(t, snapshot, feedbacks.upsertParams.Snapshot)
	require.Equal(t, "usr_1", feedbacks.upsertParams.ReporterUserID)
	require.Equal(t, []string{"incorrect"}, feedbacks.upsertParams.Reasons)
	require.Equal(t, "kqf_1", response.Body.(dto.KnowledgeQAFeedbackResponse).Feedback.ID)
}

func TestKnowledgeQATurnListProjectsCurrentViewerFeedback(t *testing.T) {
	feedbacks := &fakeFeedbackService{feedbacks: []Feedback{{
		ID: "kqf_1", TurnID: "kqt_1", Rating: "helpful", UpdatedAt: time.Now(),
	}}}
	handler := newTurnHandler(Dependencies{
		Sessions: &fakeSessionStore{turns: []Turn{{
			ID: "kqt_1", SessionID: "kqs_1", Status: "completed",
		}}},
		Feedbacks: feedbacks,
	})
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Params = gin.Params{{Key: "threadId", Value: "thr_1"}}

	response, err := handler.ListTurns(request)
	require.NoError(t, err)
	turns := response.Body.(dto.KnowledgeQATurnListResponse).Turns
	require.Len(t, turns, 1)
	require.NotNil(t, turns[0].Feedback)
	require.Equal(t, "helpful", turns[0].Feedback.Rating)
}

func TestPolicyStatusReportsUnavailableWithoutHostedWorker(t *testing.T) {
	handler := newPolicyHandler(Dependencies{
		Policies:      availablePolicy(),
		Organizations: fakeOrganizationResolver{summary: Organization{ID: "org_1"}},
	})
	response, err := handler.GetPolicyStatus(knowledgeQAWorkspaceRequest())
	require.NoError(t, err)
	payload, err := json.Marshal(response.Body)
	require.NoError(t, err)
	require.Contains(t, string(payload), `"enabled":false`)
	require.Contains(t, string(payload), `"status":"runtime_unavailable"`)
}

func TestOpenCitationUsesOwnerMetadataAndProjectsLiveAccessStatus(t *testing.T) {
	sessions := &fakeSessionStore{
		loadedSession: Session{ID: "kqs_12345678", OrganizationID: "org_1", OwnerUserID: "usr_1"},
		citation: Citation{
			ID: "kqc_1", TurnID: "kqt_1", KnowledgeBaseID: "kb_1", SourceID: "src_1", RootKind: "primary",
			RepoRole: "primary", Revision: "sha-old", Path: "docs/design.md", LineStart: 4, LineEnd: 6,
		},
	}
	opener := &fakeCitationOpener{result: forwarder.KnowledgeQACitationOpenResult{Status: "forbidden"}}
	handler := newCitationHandler(Dependencies{Sessions: sessions, Citations: opener})
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Params = gin.Params{
		{Key: "sessionId", Value: "kqs_12345678"}, {Key: "citationId", Value: "kqc_1"},
	}
	response, err := handler.OpenCitation(request)
	require.NoError(t, err)
	require.Equal(t, "forbidden", response.Body.(dto.KnowledgeQACitationOpenResponse).Status)
	require.Equal(t, "usr_1", opener.request.Principal.RequesterID)
	require.Equal(t, "org_1", opener.request.Principal.OrganizationID)
	require.Equal(t, "kqt_1", opener.request.Principal.TurnID)
	require.Equal(t, "sha-old", opener.request.Citation.Revision)
}

func TestOpenCitationPreservesExplicitUnavailableStatuses(t *testing.T) {
	for _, status := range []string{"forbidden", "source_deleted", "revision_unavailable"} {
		t.Run(status, func(t *testing.T) {
			sessions := &fakeSessionStore{
				loadedSession: Session{
					ID: "kqs_12345678", OrganizationID: "org_1", OwnerUserID: "usr_1",
				},
				citation: Citation{
					ID: "kqc_12345678", TurnID: "kqt_1", KnowledgeBaseID: "kb_1",
					SourceID: "src_1", RootKind: "primary", RepoRole: "primary",
					Revision: "sha-old", Path: "docs/design.md", LineStart: 4, LineEnd: 6,
				},
			}
			handler := newCitationHandler(Dependencies{
				Sessions: sessions,
				Citations: &fakeCitationOpener{result: forwarder.KnowledgeQACitationOpenResult{
					Status: status,
				}},
			})
			request := knowledgeQAWorkspaceRequest()
			request.Gin.Params = gin.Params{
				{Key: "sessionId", Value: "kqs_12345678"},
				{Key: "citationId", Value: "kqc_12345678"},
			}

			response, err := handler.OpenCitation(request)
			require.NoError(t, err)
			result := response.Body.(dto.KnowledgeQACitationOpenResponse)
			require.Equal(t, status, result.Status)
			require.Empty(t, result.TargetRoute)
		})
	}
}

func TestOpenCitationReturnsHistoricalSourceExcerptAndServerTarget(t *testing.T) {
	sessions := &fakeSessionStore{
		loadedSession: Session{
			ID: "kqs_12345678", OrganizationID: "org_1", OwnerUserID: "usr_1",
		},
		citation: Citation{
			ID: "kqc_12345678", TurnID: "kqt_1", KnowledgeBaseID: "kb_1",
			SourceID: "src_1", RootKind: "primary", RepoRole: "primary",
			Revision: "sha-old", Path: "docs/design.md", LineStart: 4, LineEnd: 6,
		},
	}
	handler := newCitationHandler(Dependencies{
		Sessions: sessions,
		Citations: &fakeCitationOpener{result: forwarder.KnowledgeQACitationOpenResult{
			Status: "available", OpenMode: "source", Content: "private evidence bytes", Path: "docs/design.md",
			Revision: "sha-old", LineStart: 4, LineEnd: 6,
		}},
	})
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Params = gin.Params{
		{Key: "sessionId", Value: "kqs_12345678"},
		{Key: "citationId", Value: "kqc_12345678"},
	}

	response, err := handler.OpenCitation(request)
	require.NoError(t, err)
	result := response.Body.(dto.KnowledgeQACitationOpenResponse)
	require.Equal(t, "no-store", response.Headers.Get("Cache-Control"))
	require.Equal(t, "available", result.Status)
	require.Equal(t, "source", result.OpenMode)
	require.Equal(t, "/kb/kb_1/sources/src_1/blob/sha-old/docs/design.md#L4", result.TargetRoute)
	require.Equal(t, &dto.KnowledgeQACitationEvidence{
		Content: "private evidence bytes", Path: "docs/design.md", Revision: "sha-old",
		LineStart: 4, LineEnd: 6,
	}, result.Evidence)
}

func TestOpenCitationReturnsWebTargetURLWithoutRepositoryContent(t *testing.T) {
	sessions := &fakeSessionStore{
		loadedSession: Session{
			ID: "kqs_12345678", OrganizationID: "org_1", OwnerUserID: "usr_1",
		},
		citation: Citation{
			ID: "kqc_12345678", TurnID: "kqt_1", CitationKind: "web",
			URL: "https://example.com/releases", Title: "Release notes",
		},
	}
	handler := newCitationHandler(Dependencies{
		Sessions: sessions,
		Citations: &fakeCitationOpener{result: forwarder.KnowledgeQACitationOpenResult{
			Status: "available", Content: "private web evidence bytes", TargetURL: "https://example.com/releases",
		}},
	})
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Params = gin.Params{
		{Key: "sessionId", Value: "kqs_12345678"},
		{Key: "citationId", Value: "kqc_12345678"},
	}

	response, err := handler.OpenCitation(request)
	require.NoError(t, err)
	result := response.Body.(dto.KnowledgeQACitationOpenResponse)
	require.Equal(t, "available", result.Status)
	require.Equal(t, "https://example.com/releases", result.TargetURL)
	require.Empty(t, result.TargetRoute)
	require.Nil(t, result.Evidence)
	payload, err := json.Marshal(result)
	require.NoError(t, err)
	require.NotContains(t, string(payload), "private web evidence bytes")
}

func TestOpenCitationReturnsBoundedEvidenceWithoutSourceRoute(t *testing.T) {
	sessions := &fakeSessionStore{
		loadedSession: Session{
			ID: "kqs_12345678", OrganizationID: "org_1", OwnerUserID: "usr_1",
		},
		citation: Citation{
			ID: "kqc_12345678", TurnID: "kqt_1", KnowledgeBaseID: "kb_1",
			SourceID: "src_1", RootKind: "primary", RepoRole: "primary",
			Revision: "sha-old", Path: "docs/design.md", LineStart: 4, LineEnd: 6,
		},
	}
	handler := newCitationHandler(Dependencies{
		Sessions: sessions,
		Citations: &fakeCitationOpener{result: forwarder.KnowledgeQACitationOpenResult{
			Status: "available", OpenMode: "evidence", Content: "bounded evidence\n",
			Path: "docs/design.md", Revision: "sha-old", LineStart: 4, LineEnd: 6, Truncated: true,
		}},
	})
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Params = gin.Params{
		{Key: "sessionId", Value: "kqs_12345678"},
		{Key: "citationId", Value: "kqc_12345678"},
	}

	response, err := handler.OpenCitation(request)
	require.NoError(t, err)
	result := response.Body.(dto.KnowledgeQACitationOpenResponse)
	require.Equal(t, "available", result.Status)
	require.Equal(t, "evidence", result.OpenMode)
	require.Empty(t, result.TargetRoute)
	require.Equal(t, &dto.KnowledgeQACitationEvidence{
		Content: "bounded evidence\n", Path: "docs/design.md", Revision: "sha-old",
		LineStart: 4, LineEnd: 6, Truncated: true,
	}, result.Evidence)
}

func TestOpenCitationRejectsInvalidEvidenceEnvelopeAtPublicBoundary(t *testing.T) {
	baseCitation := Citation{
		ID: "kqc_12345678", TurnID: "kqt_1", KnowledgeBaseID: "kb_1",
		SourceID: "src_1", RootKind: "primary", RepoRole: "primary",
		Revision: "sha-old", Path: "docs/design.md", LineStart: 4, LineEnd: 6,
	}
	baseResult := forwarder.KnowledgeQACitationOpenResult{
		Status: "available", OpenMode: "evidence", Content: "bounded evidence\n",
		Path: "docs/design.md", Revision: "sha-old", LineStart: 4, LineEnd: 6,
	}
	tests := []struct {
		name     string
		citation Citation
		result   forwarder.KnowledgeQACitationOpenResult
	}{
		{
			name: "path mismatch", citation: baseCitation,
			result: func() forwarder.KnowledgeQACitationOpenResult {
				result := baseResult
				result.Path = "docs/other.md"
				return result
			}(),
		},
		{
			name: "revision mismatch", citation: baseCitation,
			result: func() forwarder.KnowledgeQACitationOpenResult {
				result := baseResult
				result.Revision = "sha-newer"
				return result
			}(),
		},
		{
			name: "range mismatch", citation: baseCitation,
			result: func() forwarder.KnowledgeQACitationOpenResult {
				result := baseResult
				result.LineEnd = 7
				return result
			}(),
		},
		{
			name: "oversized content", citation: baseCitation,
			result: func() forwarder.KnowledgeQACitationOpenResult {
				result := baseResult
				result.Content = strings.Repeat("x", knowledgeQACitationMaxBytes+1)
				return result
			}(),
		},
		{
			name: "oversized line range",
			citation: func() Citation {
				citation := baseCitation
				citation.LineStart = 1
				citation.LineEnd = knowledgeQACitationMaxLines + 1
				return citation
			}(),
			result: func() forwarder.KnowledgeQACitationOpenResult {
				result := baseResult
				result.LineStart = 1
				result.LineEnd = knowledgeQACitationMaxLines + 1
				return result
			}(),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := newCitationHandler(Dependencies{
				Sessions: &fakeSessionStore{
					loadedSession: Session{
						ID: "kqs_12345678", OrganizationID: "org_1", OwnerUserID: "usr_1",
					},
					citation: tt.citation,
				},
				Citations: &fakeCitationOpener{result: tt.result},
			})
			request := knowledgeQAWorkspaceRequest()
			request.Gin.Params = gin.Params{
				{Key: "sessionId", Value: "kqs_12345678"},
				{Key: "citationId", Value: "kqc_12345678"},
			}

			response, err := handler.OpenCitation(request)
			require.NoError(t, err)
			result := response.Body.(dto.KnowledgeQACitationOpenResponse)
			require.Equal(t, "revision_unavailable", result.Status)
			require.Empty(t, result.OpenMode)
			require.Empty(t, result.TargetRoute)
			require.Nil(t, result.Evidence)
		})
	}
}

func TestOpenCitationReturnsTargetUnavailableForUnrepresentableMetadata(t *testing.T) {
	sessions := &fakeSessionStore{
		loadedSession: Session{
			ID: "kqs_12345678", OrganizationID: "org_1", OwnerUserID: "usr_1",
		},
		citation: Citation{
			ID: "kqc_12345678", TurnID: "kqt_1", KnowledgeBaseID: "kb_1",
			SourceID: "src_1", RootKind: "primary", RepoRole: "primary",
			Revision: "sha-old", Path: "../private", LineStart: 1, LineEnd: 1,
		},
	}
	handler := newCitationHandler(Dependencies{
		Sessions: sessions,
		Citations: &fakeCitationOpener{result: forwarder.KnowledgeQACitationOpenResult{
			Status: "available", OpenMode: "source",
		}},
	})
	request := knowledgeQAWorkspaceRequest()
	request.Gin.Params = gin.Params{
		{Key: "sessionId", Value: "kqs_12345678"},
		{Key: "citationId", Value: "kqc_12345678"},
	}

	response, err := handler.OpenCitation(request)
	require.NoError(t, err)
	result := response.Body.(dto.KnowledgeQACitationOpenResponse)
	require.Equal(t, "target_unavailable", result.Status)
	require.Empty(t, result.TargetRoute)
}
