/**
 * memoryTool team-pool tests (Phase 6c).
 *
 * Pins the publish / search / unpublish action contracts and the ACL
 * boundary (StateManager.getTeam lookup → membership check).
 *
 * Pins:
 *   - publish-to-team: ACL refuses non-member; copies an existing
 *     personal memory into the team's pool via indexer.publishToTeam.
 *   - search-team: ACL refuses non-member; forwards to indexer.searchTeam.
 *   - unpublish-from-team: ACL refuses non-member; forwards to
 *     indexer.unpublishFromTeam.
 *   - Without StateManager attached, all team actions fail cleanly
 *     (defaulting open would be a privacy bug).
 *   - Validation: publish needs id + teamId; search-team needs teamId + query.
 *   - Memory-not-found on publish surfaces a useful error.
 *   - Disabled embeddings → clean error, no indexer call.
 */

import { jest, describe, test, expect, beforeEach } from '@jest/globals';
import { createMockLogger } from '../../__test-utils__/mockFactories.js';

const mockMemoryService = {
  initialize: jest.fn().mockResolvedValue(undefined),
  addMemory:    jest.fn(),
  updateMemory: jest.fn(),
  appendToMemory: jest.fn(),
  deleteMemory: jest.fn(),
  listMemories: jest.fn(),
  readMemory:   jest.fn(),
  searchMemories: jest.fn(),
  getMemoryStats: jest.fn(),
};
jest.unstable_mockModule('../../services/memoryService.js', () => ({
  getMemoryService: jest.fn(() => mockMemoryService),
  PLAN_TITLE_PREFIX: 'plan/',
}));

const mockIndexerInstance = {
  isEnabled: true,
  indexOne:  jest.fn().mockResolvedValue({ indexed: true }),
  deleteOne: jest.fn().mockResolvedValue({ deleted: true }),
  indexMany: jest.fn(),
  backfill:  jest.fn(),
  search:    jest.fn().mockResolvedValue({ enabled: true, hits: [] }),
  publishToTeam:     jest.fn().mockResolvedValue({ indexed: true }),
  unpublishFromTeam: jest.fn().mockResolvedValue({ deleted: true }),
  searchTeam:        jest.fn().mockResolvedValue({ enabled: true, hits: [] }),
  teamStats:         jest.fn().mockResolvedValue({ count: 0, enabled: true }),
};
jest.unstable_mockModule('../../services/embeddings/indexers/memoryIndexer.js', () => ({
  MemoryIndexer: jest.fn(() => mockIndexerInstance),
  memoryToText: () => '',
}));

jest.unstable_mockModule('../../services/embeddings/indexers/reminisceIndexer.js', () => ({
  ReminisceIndexer: jest.fn(() => ({ isEnabled: false })),
  messageToText: () => '',
  estimateMessageTokens: () => 0,
}));

const { default: MemoryTool } = await import('../memoryTool.js');

function makeTool({
  embeddingsEnabled = true,
  team = { id: 'team-A', name: 'Team Alpha', memberAgentIds: ['agent-self', 'agent-other'] },
  withStateManager = true,
} = {}) {
  const tool = new MemoryTool({}, createMockLogger());
  mockIndexerInstance.isEnabled = embeddingsEnabled;
  for (const fn of Object.values(mockIndexerInstance)) {
    if (typeof fn?.mockClear === 'function') fn.mockClear();
  }
  for (const fn of Object.values(mockMemoryService)) {
    if (typeof fn?.mockClear === 'function') fn.mockClear();
  }
  tool.aiService = { getEmbeddingService: () => ({ isEnabled: embeddingsEnabled }) };
  tool._memoryIndexer = null;
  if (withStateManager) {
    const stateManager = {
      getTeam: jest.fn(async (id) => (team && team.id === id ? team : null)),
    };
    tool.setStateManager(stateManager);
    return { tool, stateManager };
  }
  return { tool, stateManager: null };
}

const context = { agentId: 'agent-self' };

// ─── validation ─────────────────────────────────────────────────────

describe('memoryTool — team-pool validation', () => {
  test('publish-to-team requires both id and teamId', () => {
    const { tool } = makeTool();
    const v1 = tool.customValidateParameters({ actions: [{ type: 'publish-to-team', teamId: 'team-A' }] });
    const v2 = tool.customValidateParameters({ actions: [{ type: 'publish-to-team', id: 'mem-1' }] });
    const v3 = tool.customValidateParameters({ actions: [{ type: 'publish-to-team', id: 'mem-1', teamId: 'team-A' }] });
    expect(v1.valid).toBe(false);
    expect(v2.valid).toBe(false);
    expect(v3.valid).toBe(true);
  });

  test('search-team requires teamId and query', () => {
    const { tool } = makeTool();
    expect(tool.customValidateParameters({ actions: [{ type: 'search-team', query: 'q' }] }).valid).toBe(false);
    expect(tool.customValidateParameters({ actions: [{ type: 'search-team', teamId: 'team-A' }] }).valid).toBe(false);
    expect(tool.customValidateParameters({ actions: [{ type: 'search-team', teamId: 'team-A', query: 'q' }] }).valid).toBe(true);
  });

  test('unpublish-from-team requires id and teamId', () => {
    const { tool } = makeTool();
    expect(tool.customValidateParameters({ actions: [{ type: 'unpublish-from-team', id: 'mem-1' }] }).valid).toBe(false);
    expect(tool.customValidateParameters({ actions: [{ type: 'unpublish-from-team', teamId: 'team-A' }] }).valid).toBe(false);
    expect(tool.customValidateParameters({ actions: [{ type: 'unpublish-from-team', id: 'mem-1', teamId: 'team-A' }] }).valid).toBe(true);
  });
});

// ─── ACL ────────────────────────────────────────────────────────────

describe('memoryTool — team-pool ACL', () => {
  test('non-member refused on publish-to-team', async () => {
    const { tool } = makeTool({
      team: { id: 'team-A', name: 'Team Alpha', memberAgentIds: ['agent-other'] },   // self not in
    });
    const r = await tool.execute(
      { actions: [{ type: 'publish-to-team', id: 'mem-1', teamId: 'team-A' }] },
      context,
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Access denied/i);
    expect(mockIndexerInstance.publishToTeam).not.toHaveBeenCalled();
  });

  test('non-member refused on search-team', async () => {
    const { tool } = makeTool({
      team: { id: 'team-A', memberAgentIds: [] },
    });
    const r = await tool.execute(
      { actions: [{ type: 'search-team', teamId: 'team-A', query: 'q' }] },
      context,
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Access denied/i);
    expect(mockIndexerInstance.searchTeam).not.toHaveBeenCalled();
  });

  test('non-member refused on unpublish-from-team', async () => {
    const { tool } = makeTool({
      team: { id: 'team-A', memberAgentIds: ['agent-other'] },
    });
    const r = await tool.execute(
      { actions: [{ type: 'unpublish-from-team', teamId: 'team-A', id: 'mem-1' }] },
      context,
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Access denied/i);
    expect(mockIndexerInstance.unpublishFromTeam).not.toHaveBeenCalled();
  });

  test('unknown team → clean error', async () => {
    const { tool } = makeTool({ team: null });
    const r = await tool.execute(
      { actions: [{ type: 'publish-to-team', id: 'mem-1', teamId: 'team-unknown' }] },
      context,
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/Team not found/);
  });

  test('without StateManager attached, ALL team actions refuse (no default-open)', async () => {
    const { tool } = makeTool({ withStateManager: false });
    const r1 = await tool.execute(
      { actions: [{ type: 'publish-to-team', id: 'mem-1', teamId: 'team-A' }] },
      context,
    );
    const r2 = await tool.execute(
      { actions: [{ type: 'search-team', teamId: 'team-A', query: 'q' }] },
      context,
    );
    expect(r1.success).toBe(false);
    expect(r1.error).toMatch(/StateManager/);
    expect(r2.success).toBe(false);
    expect(r2.error).toMatch(/StateManager/);
  });
});

// ─── happy paths ────────────────────────────────────────────────────

describe('memoryTool — team-pool happy paths', () => {
  test('publish-to-team: reads memory from agent\'s pool, calls indexer.publishToTeam with publishedBy', async () => {
    const { tool } = makeTool();
    const memory = { id: 'mem-1', title: 'Shared note', content: 'useful', createdAt: '2026-05-26T00:00:00Z' };
    mockMemoryService.readMemory.mockResolvedValue(memory);

    const r = await tool.execute(
      { actions: [{ type: 'publish-to-team', id: 'mem-1', teamId: 'team-A' }] },
      context,
    );

    expect(mockMemoryService.readMemory).toHaveBeenCalledWith('agent-self', 'mem-1');
    expect(mockIndexerInstance.publishToTeam).toHaveBeenCalledWith('team-A', memory, 'agent-self');
    expect(r.success).toBe(true);
    expect(r.action).toBe('publish-to-team');
    expect(r.teamId).toBe('team-A');
    expect(r.teamName).toBe('Team Alpha');
    expect(r.message).toMatch(/Published/);
  });

  test('publish-to-team: memory not found → clean error, no indexer call', async () => {
    const { tool } = makeTool();
    mockMemoryService.readMemory.mockResolvedValue(null);
    const r = await tool.execute(
      { actions: [{ type: 'publish-to-team', id: 'missing', teamId: 'team-A' }] },
      context,
    );
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/not found/i);
    expect(mockIndexerInstance.publishToTeam).not.toHaveBeenCalled();
  });

  test('search-team: forwards query + topK + hybrid to indexer', async () => {
    const { tool } = makeTool();
    mockIndexerInstance.searchTeam.mockResolvedValue({
      enabled: true,
      hits: [
        { id: 'mem-1', score: 0.9, metadata: { title: 'team note', publishedBy: 'agent-other' } },
      ],
    });

    const r = await tool.execute(
      { actions: [{ type: 'search-team', teamId: 'team-A', query: 'docker tips', topK: 3, hybrid: false }] },
      context,
    );

    expect(mockIndexerInstance.searchTeam).toHaveBeenCalledWith(
      'team-A', 'docker tips', { topK: 3, hybrid: false },
    );
    expect(r.success).toBe(true);
    expect(r.results).toHaveLength(1);
    expect(r.mode).toBe('semantic');
  });

  test('unpublish-from-team: forwards to indexer.unpublishFromTeam', async () => {
    const { tool } = makeTool();
    const r = await tool.execute(
      { actions: [{ type: 'unpublish-from-team', id: 'mem-1', teamId: 'team-A' }] },
      context,
    );
    expect(mockIndexerInstance.unpublishFromTeam).toHaveBeenCalledWith('team-A', 'mem-1');
    expect(r.success).toBe(true);
    expect(r.action).toBe('unpublish-from-team');
  });

  test('indexer error surfaces in the result without throwing', async () => {
    const { tool } = makeTool();
    mockMemoryService.readMemory.mockResolvedValue({ id: 'mem-1', title: 't', content: 'c' });
    mockIndexerInstance.publishToTeam.mockResolvedValue({ indexed: false, reason: 'error', error: 'rate-limited' });

    const r = await tool.execute(
      { actions: [{ type: 'publish-to-team', id: 'mem-1', teamId: 'team-A' }] },
      context,
    );

    expect(r.success).toBe(false);
    expect(r.error).toBe('rate-limited');
  });
});

// ─── disabled embeddings ────────────────────────────────────────────

describe('memoryTool — team-pool when embeddings disabled', () => {
  test('publish-to-team: clean error, no ACL check (no point checking when no storage)', async () => {
    const { tool, stateManager } = makeTool({ embeddingsEnabled: false });
    const r = await tool.execute(
      { actions: [{ type: 'publish-to-team', id: 'mem-1', teamId: 'team-A' }] },
      context,
    );
    expect(r.success).toBe(false);
    expect(r.message).toMatch(/not enabled/i);
    expect(stateManager.getTeam).not.toHaveBeenCalled();
  });

  test('search-team: clean error', async () => {
    const { tool } = makeTool({ embeddingsEnabled: false });
    const r = await tool.execute(
      { actions: [{ type: 'search-team', teamId: 'team-A', query: 'q' }] },
      context,
    );
    expect(r.success).toBe(false);
    expect(r.message).toMatch(/not enabled/i);
  });
});
