package memory

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/service/resource/activity"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/meshxapi"
	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	memoryservice "github.com/tidbcloud/AgentTeam/internal/service/memory"
	memorystore "github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

type fakeMemoryService struct {
	createInput memoryservice.MemoryCreateMutation
	createErr   error
	forkInput   memoryservice.MemoryForkMutation
	forkID      string
	memory      memorystore.Memory
	repos       memoryservice.MemoryRepositories
	count       int64

	batchInput   []string
	batchByAgent map[string]*memoryservice.AgentAccessibleMemory
	batchToken   string
	batchExpires time.Time
	batchErr     error

	leaseOpts   memoryservice.AgentAssetAccessEventLeaseOptions
	leaseEvents []memoryservice.AgentAssetAccessEvent

	bindingsSummary memoryservice.AgentMemoryBindingsSummary

	agentCloudMemory         memoryservice.AgentCloudMemory
	agentCloudGetAgentID     string
	agentCloudEnableAgentID  string
	agentCloudEnableInput    memoryservice.AgentCloudMemoryEnableMutation
	agentCloudDisableAgentID string

	activityOpts activity.ResourceActivityListOptions
	activityPage activity.ResourceActivityPage
}

func (s *fakeMemoryService) CreateMemory(_ context.Context, _, _ string, input memoryservice.MemoryCreateMutation) (memorystore.Memory, error) {
	s.createInput = input
	if s.createErr != nil {
		return memorystore.Memory{}, s.createErr
	}
	return s.memory, nil
}

func (s *fakeMemoryService) ForkMemory(_ context.Context, _, _ string, id string, input memoryservice.MemoryForkMutation) (memorystore.Memory, error) {
	s.forkID = id
	s.forkInput = input
	return s.memory, nil
}

func (s *fakeMemoryService) GetMemory(context.Context, string, string, string) (memorystore.Memory, error) {
	return s.memory, nil
}

func (s *fakeMemoryService) GetMemoryRepositories(context.Context, string, string, string) (memoryservice.MemoryRepositories, error) {
	return s.repos, nil
}

func (s *fakeMemoryService) ListMemories(context.Context, string, string, string, string, int, int) (memoryservice.MemoryCollectionPage, error) {
	return memoryservice.MemoryCollectionPage{}, nil
}

func (s *fakeMemoryService) CountWorkspaceMemories(context.Context, string, string) (int64, error) {
	return s.count, nil
}

func (s *fakeMemoryService) ListMemoryActivity(_ context.Context, _, _ string, opts activity.ResourceActivityListOptions) (activity.ResourceActivityPage, error) {
	s.activityOpts = opts
	return s.activityPage, nil
}

func (s *fakeMemoryService) GetAgentAccessibleMemory(context.Context, string, string, string) (*memoryservice.AgentAccessibleMemory, error) {
	return nil, nil
}

func (s *fakeMemoryService) BatchListAgentAccessibleMemories(_ context.Context, _, _ string, agentIDs []string) (memoryservice.AgentMemoryAccessBatch, error) {
	s.batchInput = agentIDs
	if s.batchErr != nil {
		return memoryservice.AgentMemoryAccessBatch{}, s.batchErr
	}
	if s.batchByAgent == nil {
		out := make(map[string]*memoryservice.AgentAccessibleMemory, len(agentIDs))
		for _, id := range agentIDs {
			out[id] = nil
		}
		return memoryservice.AgentMemoryAccessBatch{Agents: out, SyncToken: s.batchToken, SyncTokenExpires: s.batchExpires}, nil
	}
	return memoryservice.AgentMemoryAccessBatch{Agents: s.batchByAgent, SyncToken: s.batchToken, SyncTokenExpires: s.batchExpires}, nil
}

func (s *fakeMemoryService) GetAgentCloudMemory(_ context.Context, _, _, agentID string) (memoryservice.AgentCloudMemory, error) {
	s.agentCloudGetAgentID = agentID
	return s.agentCloudMemory, nil
}

func (s *fakeMemoryService) EnableAgentCloudMemory(_ context.Context, _, _, agentID string, input memoryservice.AgentCloudMemoryEnableMutation) (memoryservice.AgentCloudMemory, error) {
	s.agentCloudEnableAgentID = agentID
	s.agentCloudEnableInput = input
	return s.agentCloudMemory, nil
}

func (s *fakeMemoryService) DisableAgentCloudMemory(_ context.Context, _, _, agentID string) (memoryservice.AgentCloudMemory, error) {
	s.agentCloudDisableAgentID = agentID
	return s.agentCloudMemory, nil
}

func (s *fakeMemoryService) ListAgentCloudMemoryCommits(context.Context, string, string, string, memoryservice.AgentCloudMemoryCommitListOptions) (json.RawMessage, error) {
	return json.RawMessage(`[]`), nil
}

func (s *fakeMemoryService) GetAgentCloudMemoryCompare(context.Context, string, string, string, memoryservice.AgentCloudMemoryCompareOptions) (json.RawMessage, error) {
	return json.RawMessage(`{}`), nil
}

func (s *fakeMemoryService) ListAgentCloudMemoryBackups(context.Context, string, string, string, string, int, int) (memoryservice.AgentCloudMemoryBackupPage, error) {
	return memoryservice.AgentCloudMemoryBackupPage{}, nil
}

func (s *fakeMemoryService) CreateAgentCloudMemoryBackup(context.Context, string, string, string, memoryservice.AgentCloudMemoryBackupCreateMutation) (memoryservice.AgentCloudMemoryBackup, error) {
	return memoryservice.AgentCloudMemoryBackup{}, nil
}

func (s *fakeMemoryService) RestoreAgentCloudMemoryBackup(context.Context, string, string, string, string) (memoryservice.AgentCloudMemoryBackup, error) {
	return memoryservice.AgentCloudMemoryBackup{}, nil
}

func (s *fakeMemoryService) BindAgentMemory(context.Context, string, string, string, memoryservice.AgentMemoryBindingMutation) (memoryservice.AgentMemoryBindingResult, error) {
	return memoryservice.AgentMemoryBindingResult{}, nil
}

func (s *fakeMemoryService) UnbindAgentMemory(context.Context, string, string, string) (memoryservice.AgentMemoryBindingResult, error) {
	return memoryservice.AgentMemoryBindingResult{}, nil
}

func (s *fakeMemoryService) ListAgentMemoryBindings(context.Context, string, string) (memoryservice.AgentMemoryBindingsSummary, error) {
	return s.bindingsSummary, nil
}

func (s *fakeMemoryService) LeaseAgentAssetAccessEvents(_ context.Context, opts memoryservice.AgentAssetAccessEventLeaseOptions) ([]memoryservice.AgentAssetAccessEvent, error) {
	s.leaseOpts = opts
	return s.leaseEvents, nil
}

func (s *fakeMemoryService) MarkAgentAssetAccessEventDelivered(context.Context, string, string, time.Time) (bool, error) {
	return true, nil
}

func (s *fakeMemoryService) MarkAgentAssetAccessEventFailed(context.Context, memoryservice.AgentAssetAccessEventFailure) (memorystore.AgentAssetAccessAttemptResult, error) {
	return memorystore.AgentAssetAccessAttemptResult{Updated: true}, nil
}

func (s *fakeMemoryService) UpdateMemory(context.Context, string, string, string, memoryservice.MemoryUpdateMutation) (memorystore.Memory, error) {
	return s.memory, nil
}

func (s *fakeMemoryService) DeleteMemory(context.Context, string, string, string) error {
	return nil
}

func (s *fakeMemoryService) PinMemory(context.Context, string, string, string) error {
	return nil
}

func (s *fakeMemoryService) UnpinMemory(context.Context, string, string, string) error {
	return nil
}

func (s *fakeMemoryService) ListMemoryPermissions(context.Context, string, string, string) (memoryservice.MemoryPermissionsSummary, error) {
	return memoryservice.MemoryPermissionsSummary{}, nil
}

func (s *fakeMemoryService) UpsertMemoryPermission(context.Context, string, string, string, memoryservice.MemoryPermissionGrantMutation) (memoryservice.MemoryPermissionGrant, error) {
	return memoryservice.MemoryPermissionGrant{}, nil
}

func (s *fakeMemoryService) DeleteMemoryPermission(context.Context, string, string, string, string) error {
	return nil
}

func TestCreateMemoryPassesRepositoryActor(t *testing.T) {
	now := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC)
	svc := &fakeMemoryService{memory: memorystore.Memory{
		ID:        "mem_1",
		Slug:      "runbooks",
		Name:      "Runbooks",
		IsActive:  true,
		CreatedBy: "usr_1",
		UpdatedBy: "usr_1",
		CreatedAt: now,
		UpdatedAt: now,
	}}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodPost, "/api/v1/memory", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	response, err := handler.Create(meshxapi.AuthenticatedJSON[dto.MemoryCreateRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request: request,
			Identity: trusted.Identity{
				Subject:           "usr_1",
				PreferredUsername: "Lloyd User",
				Name:              "Lloyd User",
				Email:             "lloyd@example.test",
				Groups:            []string{"engineering"},
			},
		},
		Body: dto.MemoryCreateRequest{Name: "Runbooks", TemplateLocale: "zh-CN"},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, response.Status)
	require.Equal(t, "Runbooks", svc.createInput.Name)
	require.Equal(t, "zh-CN", svc.createInput.TemplateLocale)
	require.Equal(t, repos.ResourceRepositoryActor{
		Provider: "agentteam",
		Subject:  "usr_1",
		Login:    "lloyd-user",
		Name:     "Lloyd User",
		Email:    "lloyd@example.test",
		Groups:   []string{"engineering"},
	}, svc.createInput.Actor)
}

func TestForkMemoryPassesRepositoryActorAndSourceID(t *testing.T) {
	now := time.Date(2026, 6, 17, 1, 20, 0, 0, time.UTC)
	svc := &fakeMemoryService{memory: memorystore.Memory{
		ID:        "mem_fork",
		Slug:      "runbooks-fork",
		Name:      "Runbooks fork",
		IsActive:  true,
		Role:      "owner",
		CanManage: true,
		CreatedBy: "usr_1",
		UpdatedBy: "usr_1",
		CreatedAt: now,
		UpdatedAt: now,
	}}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodPost, "/api/v1/memory/mem_source/fork", nil)
	require.NoError(t, err)
	request.SetPathValue("memory_id", "mem_source")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	response, err := handler.Fork(meshxapi.AuthenticatedJSON[dto.MemoryForkRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request: request,
			Identity: trusted.Identity{
				Subject:           "usr_1",
				PreferredUsername: "Lloyd User",
				Name:              "Lloyd User",
				Email:             "lloyd@example.test",
				Groups:            []string{"engineering"},
			},
		},
		Body: dto.MemoryForkRequest{Name: "Runbooks fork", RepoNameOverride: "runbooks-fork"},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, response.Status)
	require.Equal(t, "mem_source", svc.forkID)
	require.Equal(t, "Runbooks fork", svc.forkInput.Name)
	require.Equal(t, "runbooks-fork", svc.forkInput.RepoNameOverride)
	require.Equal(t, repos.ResourceRepositoryActor{
		Provider: "agentteam",
		Subject:  "usr_1",
		Login:    "lloyd-user",
		Name:     "Lloyd User",
		Email:    "lloyd@example.test",
		Groups:   []string{"engineering"},
	}, svc.forkInput.Actor)
	body, ok := response.Body.(dto.Memory)
	require.True(t, ok)
	require.Equal(t, "mem_fork", body.ID)
}

func TestGetMemoryIncludesAccessAndPinMetadata(t *testing.T) {
	now := time.Date(2026, 6, 14, 12, 0, 0, 0, time.UTC)
	pinnedAt := now.Add(time.Minute)
	svc := &fakeMemoryService{memory: memorystore.Memory{
		ID:        "mem_1",
		Slug:      "runbooks",
		Name:      "Runbooks",
		IsActive:  true,
		Pinned:    true,
		PinnedAt:  &pinnedAt,
		Role:      "manage",
		CanManage: true,
		CreatedBy: "usr_1",
		UpdatedBy: "usr_1",
		CreatedAt: now,
		UpdatedAt: now,
	}}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodGet, "/api/v1/memory/mem_1", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	response, err := handler.Get(meshxapi.AuthenticatedRequest{
		Request: request,
		Identity: trusted.Identity{
			Subject: "usr_1",
		},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	body, ok := response.Body.(dto.Memory)
	require.True(t, ok)
	require.Equal(t, "manage", body.Role)
	require.True(t, body.CanManage)
	require.True(t, body.Pinned)
	require.NotEmpty(t, body.PinnedAt)
}

func TestAgentAccessBatchEmptyRequestReturnsEmptyMap(t *testing.T) {
	svc := &fakeMemoryService{}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodPost, "/api/v1/memory/agents/access:batch", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	response, err := handler.AgentAccessBatch(meshxapi.AuthenticatedJSON[dto.AgentAccessBatchRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request:  request,
			Identity: trusted.Identity{Subject: "usr_alice"},
		},
		Body: dto.AgentAccessBatchRequest{AgentIDs: nil},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	body, ok := response.Body.(dto.AgentAccessBatchResponse)
	require.True(t, ok)
	require.NotNil(t, body.Agents)
	require.Empty(t, body.Agents)
	require.Empty(t, body.SyncToken)
	require.Nil(t, svc.batchInput)
}

func TestAgentAccessBatchReturnsPerAgentMemoriesWithRepositoryRefs(t *testing.T) {
	now := time.Date(2026, 6, 15, 9, 30, 0, 0, time.UTC)
	canonicalAgent := &reposstore.ResourceRepository{
		ID:           "repo_mem_alpha",
		WorkspaceID:  "ws_1",
		ResourceType: reposstore.ResourceTypeMemory,
		ResourceID:   "mem_alpha",
		RepoRole:     reposstore.RepoRolePrimary,
		OwnerKind:    "mem",
		RepoOwner:    "user-1",
		RepoName:     "alice-runbooks",
		RepoFullName: "user-1/alice-runbooks.mem",
		AGSRepoID:    "ags_repo_alpha",
		DefaultRef:   "main",
	}
	svc := &fakeMemoryService{
		batchToken:   "ags_rat_memory",
		batchExpires: time.Date(2026, 6, 15, 10, 0, 0, 0, time.UTC),
		batchByAgent: map[string]*memoryservice.AgentAccessibleMemory{
			"agt_alice": {
				ID:              "mem_alpha",
				Name:            "Alice Runbooks",
				Description:     "Personal agent runbooks",
				AccessLevel:     "read",
				PermissionSetAt: now,
				UpdatedAt:       now,
				Repository:      canonicalAgent,
				Branch:          "main",
				SyncProtocol:    memoryservice.MemorySyncProtocolLegacyGitV1,
			},
			"agt_bob": nil,
		},
	}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodPost, "/api/v1/memory/agents/access:batch", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	response, err := handler.AgentAccessBatch(meshxapi.AuthenticatedJSON[dto.AgentAccessBatchRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request:  request,
			Identity: trusted.Identity{Subject: "usr_alice"},
		},
		Body: dto.AgentAccessBatchRequest{AgentIDs: []string{"agt_alice", "agt_bob"}},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	body, ok := response.Body.(dto.AgentAccessBatchResponse)
	require.True(t, ok)
	require.Equal(t, "ags_rat_memory", body.SyncToken)
	require.NotEmpty(t, body.SyncTokenExpires)
	require.ElementsMatch(t, []string{"agt_alice", "agt_bob"}, svc.batchInput)
	require.Contains(t, body.Agents, "agt_alice")
	require.NotNil(t, body.Agents["agt_alice"])
	item := body.Agents["agt_alice"]
	require.Equal(t, "mem_alpha", item.ID)
	require.Equal(t, memoryservice.MemorySyncProtocolLegacyGitV1, item.SyncProtocol)
	require.Equal(t, "main", item.Branch)
	require.NotNil(t, item.Repository)
	require.Equal(t, "user-1/alice-runbooks.mem", item.Repository.RepoFullName)
	require.Equal(t, "ags_repo_alpha", item.Repository.RepoID)
	require.Equal(t, "main", item.Repository.DefaultRef)
	require.Contains(t, body.Agents, "agt_bob")
	require.Nil(t, body.Agents["agt_bob"])
}

func TestAgentAccessBatchReturnsNativeProjectionWithoutAGSCredential(t *testing.T) {
	svc := &fakeMemoryService{
		batchToken: "",
		batchByAgent: map[string]*memoryservice.AgentAccessibleMemory{
			"agt_alice": {
				ID:                   "mem_alpha",
				Name:                 "Alice Runbooks",
				AccessLevel:          "write",
				SyncProtocol:         memoryservice.MemorySyncProtocolNativeContentV1,
				ContentID:            "ctn_alpha",
				ContentVersionID:     "ctv_alpha",
				ContentGeneration:    7,
				AcknowledgedSequence: 23,
				ActiveReplicaID:      "replica-desktop",
			},
		},
	}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodPost, "/api/v1/memory/agents/access:batch", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	response, err := handler.AgentAccessBatch(meshxapi.AuthenticatedJSON[dto.AgentAccessBatchRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_alice"}},
		Body:                 dto.AgentAccessBatchRequest{AgentIDs: []string{"agt_alice"}},
	})

	require.NoError(t, err)
	body, ok := response.Body.(dto.AgentAccessBatchResponse)
	require.True(t, ok)
	require.Empty(t, body.SyncToken)
	item := body.Agents["agt_alice"]
	require.NotNil(t, item)
	require.Equal(t, memoryservice.MemorySyncProtocolNativeContentV1, item.SyncProtocol)
	require.Nil(t, item.Repository)
	require.Empty(t, item.Branch)
	require.Equal(t, "ctn_alpha", item.ContentID)
	require.Equal(t, "ctv_alpha", item.ContentVersionID)
	require.EqualValues(t, 7, item.ContentGeneration)
	require.EqualValues(t, 23, item.AcknowledgedSequence)
	require.Equal(t, "replica-desktop", item.ActiveReplicaID)
}

func TestGetAgentCloudMemoryReturnsRepositoryRef(t *testing.T) {
	now := time.Date(2026, 6, 15, 9, 30, 0, 0, time.UTC)
	svc := &fakeMemoryService{
		agentCloudMemory: memoryservice.AgentCloudMemory{
			AgentID:  "agt_alice",
			Enabled:  true,
			MemoryID: "mem_alpha",
			Name:     "Alice memory",
			Branch:   "main",
			Repository: &reposstore.ResourceRepository{
				ID:           "repo_mem_alpha",
				RepoOwner:    "user-1",
				RepoName:     "alice-memory.mem",
				RepoFullName: "user-1/alice-memory.mem",
				AGSRepoID:    "ags_repo_alpha",
				DefaultRef:   "main",
				UpdatedAt:    now,
			},
			Status:     "enabled",
			SyncStatus: "automatic",
			LastSyncAt: now,
		},
	}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodGet, "/api/v1/memory/agents/agt_alice/cloud-memory", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")
	request.SetPathValue("agent_id", "agt_alice")

	response, err := handler.GetAgentCloudMemory(meshxapi.AuthenticatedRequest{
		Request:  request,
		Identity: trusted.Identity{Subject: "usr_alice"},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	body, ok := response.Body.(dto.AgentCloudMemory)
	require.True(t, ok)
	require.Equal(t, "agt_alice", svc.agentCloudGetAgentID)
	require.NotNil(t, body.Repository)
	require.Equal(t, "user-1/alice-memory.mem", body.Repository.RepoFullName)
	require.Equal(t, "ags_repo_alpha", body.Repository.RepoID)
	require.Equal(t, "main", body.Repository.DefaultRef)
}

func TestEnableAgentCloudMemoryAcceptsTemplateLocale(t *testing.T) {
	svc := &fakeMemoryService{
		agentCloudMemory: memoryservice.AgentCloudMemory{
			AgentID:    "agt_alice",
			Enabled:    true,
			Status:     "enabled",
			SyncStatus: "automatic",
		},
	}
	mux := http.NewServeMux()
	RegisterRoutes(mux, NewMemoryHandler(svc), nil)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/memory/agents/agt_alice/cloud-memory", bytes.NewBufferString(`{"importExistingLocalContent":true,"templateLocale":"ja"}`))
	req = req.WithContext(trusted.WithIdentity(req.Context(), trusted.Identity{Subject: "usr_1"}))
	req.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")
	rec := httptest.NewRecorder()

	mux.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	require.Equal(t, "agt_alice", svc.agentCloudEnableAgentID)
	require.NotNil(t, svc.agentCloudEnableInput.ImportExistingLocalContent)
	require.True(t, *svc.agentCloudEnableInput.ImportExistingLocalContent)
	require.Equal(t, "ja", svc.agentCloudEnableInput.TemplateLocale)
}

func TestAgentAccessBatchRejectsTooManyAgentIDs(t *testing.T) {
	svc := &fakeMemoryService{}
	handler := NewMemoryHandler(svc)
	tooMany := make([]string, batchAgentIDsLimit+1)
	for i := range tooMany {
		tooMany[i] = "agt_" + string(rune('a'+i%26)) + string(rune('0'+i/26))
	}
	request, err := http.NewRequest(http.MethodPost, "/api/v1/memory/agents/access:batch", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	_, err = handler.AgentAccessBatch(meshxapi.AuthenticatedJSON[dto.AgentAccessBatchRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request:  request,
			Identity: trusted.Identity{Subject: "usr_alice"},
		},
		Body: dto.AgentAccessBatchRequest{AgentIDs: tooMany},
	})

	require.Error(t, err)
	httpErr, ok := err.(meshxapi.HTTPError)
	require.True(t, ok)
	require.Equal(t, http.StatusBadRequest, httpErr.Status)
}

func TestAgentAccessBatchForwardsRequestAsIs(t *testing.T) {
	svc := &fakeMemoryService{
		batchByAgent: map[string]*memoryservice.AgentAccessibleMemory{
			"agt_alice": nil,
		},
	}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodPost, "/api/v1/memory/agents/access:batch", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	response, err := handler.AgentAccessBatch(meshxapi.AuthenticatedJSON[dto.AgentAccessBatchRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request:  request,
			Identity: trusted.Identity{Subject: "usr_alice"},
		},
		Body: dto.AgentAccessBatchRequest{AgentIDs: []string{"agt_alice", "agt_alice", "agt_bob"}},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	// Handler passes the request through; service-layer dedup is verified in
	// service tests.
	require.Equal(t, []string{"agt_alice", "agt_alice", "agt_bob"}, svc.batchInput)
}

func TestLeaseAgentAssetAccessEventsReturnsUnifiedPayloadForServiceActor(t *testing.T) {
	occurredAt := time.Date(2026, 6, 20, 3, 40, 0, 0, time.UTC)
	svc := &fakeMemoryService{
		leaseEvents: []memoryservice.AgentAssetAccessEvent{
			{
				EventID:     "aaac_1",
				WorkspaceID: "ws_1",
				AgentIDs:    []string{"agt_alice"},
				AssetTypes:  []string{memorystore.AssetTypeMemory},
				Version:     123,
				OccurredAt:  occurredAt,
				Source:      memorystore.AgentAssetAccessSourceOutbox,
				Attempts:    1,
				MaxAttempts: 10,
			},
		},
	}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodPost, "/api/v1/internal/agent-assets/access-events:lease", nil)
	require.NoError(t, err)

	response, err := handler.LeaseAgentAssetAccessEvents(meshxapi.AuthenticatedJSON[dto.AgentAssetAccessEventLeaseRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request: request,
			Identity: trusted.Identity{
				Subject: "svc_asset_access",
				RawClaims: map[string]any{
					"service_actor":          true,
					"automation_permissions": []any{"automation:service"},
				},
			},
		},
		Body: dto.AgentAssetAccessEventLeaseRequest{LeaseOwner: "daemon-1", LeaseSeconds: 30, Limit: 5},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	require.Equal(t, "daemon-1", svc.leaseOpts.LeaseOwner)
	require.Equal(t, 30*time.Second, svc.leaseOpts.LeaseDuration)
	require.Equal(t, 5, svc.leaseOpts.Limit)
	body, ok := response.Body.(dto.AgentAssetAccessEventLeaseResponse)
	require.True(t, ok)
	require.Len(t, body.Events, 1)
	require.Equal(t, "aaac_1", body.Events[0].EventID)
	require.Equal(t, []string{"agt_alice"}, body.Events[0].AgentIDs)
	require.Equal(t, []string{memorystore.AssetTypeMemory}, body.Events[0].AssetTypes)
	require.Equal(t, int64(123), body.Events[0].Version)
	require.Equal(t, memorystore.AgentAssetAccessSourceOutbox, body.Events[0].Source)
	require.Equal(t, occurredAt.UTC().Format(time.RFC3339Nano), body.Events[0].OccurredAt)
}

func TestLeaseAgentAssetAccessEventsRejectsNonServiceActor(t *testing.T) {
	handler := NewMemoryHandler(&fakeMemoryService{})
	request, err := http.NewRequest(http.MethodPost, "/api/v1/internal/agent-assets/access-events:lease", nil)
	require.NoError(t, err)

	_, err = handler.LeaseAgentAssetAccessEvents(meshxapi.AuthenticatedJSON[dto.AgentAssetAccessEventLeaseRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request:  request,
			Identity: trusted.Identity{Subject: "usr_alice"},
		},
		Body: dto.AgentAssetAccessEventLeaseRequest{LeaseOwner: "daemon-1"},
	})

	require.Error(t, err)
	httpErr, ok := err.(meshxapi.HTTPError)
	require.True(t, ok)
	require.Equal(t, http.StatusForbidden, httpErr.Status)
}

func TestListAgentMemoryBindingsReturnsBindingsAndMemoryOccupancy(t *testing.T) {
	now := time.Date(2026, 6, 16, 11, 0, 0, 0, time.UTC)
	svc := &fakeMemoryService{
		bindingsSummary: memoryservice.AgentMemoryBindingsSummary{
			OwnedAgents: []memoryservice.AgentOwnership{
				{ID: "agt_alice", Name: "looper-alice", DisplayName: "Alice"},
			},
			Bindings: []memoryservice.AgentMemoryBindingSummary{
				{
					ID:                         "mab_alice",
					AgentID:                    "agt_alice",
					MemoryID:                   "mem_alpha",
					Branch:                     "main",
					ImportExistingLocalContent: true,
					CreatedBy:                  "usr_alice",
					UpdatedBy:                  "usr_alice",
					CreatedAt:                  now,
					UpdatedAt:                  now,
				},
			},
			Memories: []memoryservice.AgentMemoryBindingMemory{
				{
					ID:                         "mem_alpha",
					Slug:                       "alpha",
					Name:                       "Alpha",
					IsActive:                   true,
					Role:                       "owner",
					CanManage:                  true,
					CanBind:                    false,
					BoundAgentID:               "agt_alice",
					BoundAgentName:             "Alice",
					BoundBranch:                "main",
					ImportExistingLocalContent: true,
					UpdatedAt:                  now,
				},
				{
					ID:        "mem_beta",
					Slug:      "beta",
					Name:      "Beta",
					IsActive:  true,
					Role:      "owner",
					CanManage: true,
					CanBind:   true,
					Repository: &reposstore.ResourceRepository{
						ID:           "repo_beta",
						RepoFullName: "user-1/beta.mem",
						DefaultRef:   "main",
					},
					UpdatedAt: now,
				},
			},
		},
	}
	handler := NewMemoryHandler(svc)
	request, err := http.NewRequest(http.MethodGet, "/api/v1/memory/agents/bindings", nil)
	require.NoError(t, err)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")

	response, err := handler.ListAgentMemoryBindings(meshxapi.AuthenticatedRequest{
		Request:  request,
		Identity: trusted.Identity{Subject: "usr_alice"},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	body, ok := response.Body.(dto.AgentMemoryBindingsResponse)
	require.True(t, ok)
	require.Len(t, body.OwnedAgents, 1)
	require.Equal(t, "agt_alice", body.OwnedAgents[0].ID)
	require.Equal(t, "Alice", body.OwnedAgents[0].DisplayName)
	require.Len(t, body.Bindings, 1)
	require.Equal(t, "mem_alpha", body.Bindings[0].MemoryID)
	require.Equal(t, "main", body.Bindings[0].Branch)
	require.True(t, body.Bindings[0].ImportExistingLocalContent)
	require.Len(t, body.Memories, 2)
	require.Equal(t, "mem_alpha", body.Memories[0].ID)
	require.False(t, body.Memories[0].CanBind)
	require.Equal(t, "agt_alice", body.Memories[0].BoundAgentID)
	require.Equal(t, "Alice", body.Memories[0].BoundAgentName)
	require.Equal(t, "main", body.Memories[0].BoundBranch)
	require.Equal(t, "mem_beta", body.Memories[1].ID)
	require.True(t, body.Memories[1].CanBind)
	require.NotNil(t, body.Memories[1].Repository)
	require.Equal(t, "user-1/beta.mem", body.Memories[1].Repository.RepoFullName)
}

func TestMemoryActivityRouteReturnsCursorPage(t *testing.T) {
	now := time.Date(2026, 6, 22, 10, 0, 0, 0, time.UTC)
	svc := &fakeMemoryService{activityPage: activity.ResourceActivityPage{
		Items: []activity.ResourceActivityItem{{
			ID:          "act_1",
			WorkspaceID: "ws_1",
			AssetType:   "memory",
			AssetID:     "mem_1",
			AssetName:   "Runbooks",
			EventType:   "created",
			Actor:       activity.ResourceActivityActor{Type: "user", ID: "usr_1", Name: "Alice"},
			SummaryKey:  "memory.created",
			Details:     map[string]any{"name": "Runbooks"},
			Source:      "control",
			OccurredAt:  now,
		}},
		NextCursor: "cursor_2",
	}}
	mux := http.NewServeMux()
	RegisterRoutes(mux, NewMemoryHandler(svc), nil)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/memory/activity?assetId=mem_1&eventType=created&actorId=usr_1&limit=25", nil)
	req = req.WithContext(trusted.WithIdentity(req.Context(), trusted.Identity{Subject: "usr_1"}))
	req.Header.Set(meshxapi.HeaderWorkspaceID, "ws_1")
	rec := httptest.NewRecorder()

	mux.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	require.Equal(t, activity.ResourceActivityListOptions{AssetID: "mem_1", EventType: "created", ActorID: "usr_1", Limit: 25}, svc.activityOpts)
	require.Contains(t, rec.Body.String(), `"items"`)
	require.Contains(t, rec.Body.String(), `"nextCursor":"cursor_2"`)
	require.Contains(t, rec.Body.String(), `"summaryKey":"memory.created"`)
}
