import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import {
  createPgliteDb,
  insights,
  missionTasks,
  missions,
  preferences,
  projects,
  runPgliteMigrations,
  teams,
  users,
  type Db,
  type DbHandle,
} from '@mosaicstack/db';
import type { FederationScopeQueryFilter } from '../../scope.service.js';
import { FederationListQueryService } from '../list-query.service.js';

const TASK_FILTER: FederationScopeQueryFilter = {
  resource: 'tasks',
  subjectUserId: 'user-1',
  includePersonal: true,
  teamIds: [],
  limit: 2,
  maxRowsPerQuery: 2,
};

const SUBJECT_USER_ID = 'fed-m3-05-subject';
const OTHER_USER_ID = 'fed-m3-05-other';
const TEAM_ID = '05000000-0000-4000-8000-000000000001';
const UNAUTHORIZED_TEAM_ID = '05000000-0000-4000-8000-000000000002';
const PERSONAL_PROJECT_ID = '05000000-0000-4000-8000-000000000101';
const TEAM_PROJECT_ID = '05000000-0000-4000-8000-000000000102';
const UNAUTHORIZED_PROJECT_ID = '05000000-0000-4000-8000-000000000103';
const PERSONAL_MISSION_ID = '05000000-0000-4000-8000-000000000201';
const TEAM_MISSION_ID = '05000000-0000-4000-8000-000000000202';
const UNAUTHORIZED_MISSION_ID = '05000000-0000-4000-8000-000000000203';
const SUBJECT_TEAM_NOTE_ID = '05000000-0000-4000-8000-000000000301';
const OTHER_TEAM_NOTE_ID = '05000000-0000-4000-8000-000000000302';
const SUBJECT_PERSONAL_NOTE_ID = '05000000-0000-4000-8000-000000000303';
const SUBJECT_UNAUTHORIZED_NOTE_ID = '05000000-0000-4000-8000-000000000304';
const INSIGHT_ONE_ID = '05000000-0000-4000-8000-000000000401';
const INSIGHT_TWO_ID = '05000000-0000-4000-8000-000000000402';
const PREFERENCE_ONE_ID = '05000000-0000-4000-8000-000000000501';
const PREFERENCE_TWO_ID = '05000000-0000-4000-8000-000000000502';

let dbHandle: DbHandle | undefined;

function makeService() {
  return new FederationListQueryService({} as Db);
}

function makeDbService() {
  if (!dbHandle) {
    throw new Error('test DB not initialized');
  }
  return new FederationListQueryService(dbHandle.db);
}

async function seedNotesFixture() {
  if (!dbHandle) {
    throw new Error('test DB not initialized');
  }

  await dbHandle.db.insert(users).values([
    {
      id: SUBJECT_USER_ID,
      name: 'Federation Subject',
      email: `${SUBJECT_USER_ID}@example.test`,
      emailVerified: false,
    },
    {
      id: OTHER_USER_ID,
      name: 'Federation Other',
      email: `${OTHER_USER_ID}@example.test`,
      emailVerified: false,
    },
  ]);

  await dbHandle.db.insert(teams).values([
    {
      id: TEAM_ID,
      name: 'FED-M3-05 Team',
      slug: 'fed-m3-05-team',
      ownerId: SUBJECT_USER_ID,
      managerId: SUBJECT_USER_ID,
    },
    {
      id: UNAUTHORIZED_TEAM_ID,
      name: 'FED-M3-05 Unauthorized Team',
      slug: 'fed-m3-05-unauthorized-team',
      ownerId: OTHER_USER_ID,
      managerId: OTHER_USER_ID,
    },
  ]);

  await dbHandle.db.insert(projects).values([
    {
      id: PERSONAL_PROJECT_ID,
      name: 'FED-M3-05 Personal Project',
      ownerId: SUBJECT_USER_ID,
      ownerType: 'user',
    },
    {
      id: TEAM_PROJECT_ID,
      name: 'FED-M3-05 Team Project',
      teamId: TEAM_ID,
      ownerType: 'team',
    },
    {
      id: UNAUTHORIZED_PROJECT_ID,
      name: 'FED-M3-05 Unauthorized Project',
      teamId: UNAUTHORIZED_TEAM_ID,
      ownerType: 'team',
    },
  ]);

  await dbHandle.db.insert(missions).values([
    {
      id: PERSONAL_MISSION_ID,
      name: 'FED-M3-05 Personal Mission',
      projectId: PERSONAL_PROJECT_ID,
      userId: SUBJECT_USER_ID,
    },
    {
      id: TEAM_MISSION_ID,
      name: 'FED-M3-05 Team Mission',
      projectId: TEAM_PROJECT_ID,
      userId: SUBJECT_USER_ID,
    },
    {
      id: UNAUTHORIZED_MISSION_ID,
      name: 'FED-M3-05 Unauthorized Mission',
      projectId: UNAUTHORIZED_PROJECT_ID,
      userId: SUBJECT_USER_ID,
    },
  ]);

  await dbHandle.db.insert(missionTasks).values([
    {
      id: SUBJECT_TEAM_NOTE_ID,
      missionId: TEAM_MISSION_ID,
      userId: SUBJECT_USER_ID,
      notes: 'subject note on team mission',
      createdAt: new Date('2026-06-24T03:00:00.000Z'),
      updatedAt: new Date('2026-06-24T03:00:00.000Z'),
    },
    {
      id: OTHER_TEAM_NOTE_ID,
      missionId: TEAM_MISSION_ID,
      userId: OTHER_USER_ID,
      notes: 'other user note on team mission',
      createdAt: new Date('2026-06-24T02:00:00.000Z'),
      updatedAt: new Date('2026-06-24T02:00:00.000Z'),
    },
    {
      id: SUBJECT_PERSONAL_NOTE_ID,
      missionId: PERSONAL_MISSION_ID,
      userId: SUBJECT_USER_ID,
      notes: 'subject note on personal mission',
      createdAt: new Date('2026-06-24T01:00:00.000Z'),
      updatedAt: new Date('2026-06-24T01:00:00.000Z'),
    },
    {
      id: SUBJECT_UNAUTHORIZED_NOTE_ID,
      missionId: UNAUTHORIZED_MISSION_ID,
      userId: SUBJECT_USER_ID,
      notes: 'subject note outside grant-visible missions',
      createdAt: new Date('2026-06-24T04:00:00.000Z'),
      updatedAt: new Date('2026-06-24T04:00:00.000Z'),
    },
  ]);

  const memoryCreatedAt = new Date('2026-06-24T05:00:00.000Z');
  await dbHandle.db.insert(insights).values([
    {
      id: INSIGHT_ONE_ID,
      userId: SUBJECT_USER_ID,
      content: 'first insight',
      source: 'agent',
      createdAt: memoryCreatedAt,
      updatedAt: memoryCreatedAt,
    },
    {
      id: INSIGHT_TWO_ID,
      userId: SUBJECT_USER_ID,
      content: 'second insight',
      source: 'agent',
      createdAt: memoryCreatedAt,
      updatedAt: memoryCreatedAt,
    },
  ]);

  await dbHandle.db.insert(preferences).values([
    {
      id: PREFERENCE_ONE_ID,
      userId: SUBJECT_USER_ID,
      key: 'fed-m3-05-pref-1',
      value: { enabled: true },
      createdAt: memoryCreatedAt,
      updatedAt: memoryCreatedAt,
    },
    {
      id: PREFERENCE_TWO_ID,
      userId: SUBJECT_USER_ID,
      key: 'fed-m3-05-pref-2',
      value: { enabled: false },
      createdAt: memoryCreatedAt,
      updatedAt: memoryCreatedAt,
    },
  ]);
}

function stubRows(
  service: FederationListQueryService,
  ...pages: Array<Array<Record<string, unknown>>>
) {
  const mock = vi.fn();
  for (const page of pages) {
    mock.mockResolvedValueOnce(page);
  }
  (
    service as unknown as {
      listAllRows: (
        _filter: FederationScopeQueryFilter,
        _rowLimit: number,
        _cursor: unknown,
      ) => Promise<Array<Record<string, unknown>>>;
    }
  ).listAllRows = mock;
  return mock;
}

describe('FederationListQueryService', () => {
  beforeAll(async () => {
    dbHandle = createPgliteDb(`memory://fed-m3-05-list-${Date.now()}`);
    await runPgliteMigrations(dbHandle);
    await seedNotesFixture();
  });

  afterAll(async () => {
    await dbHandle?.close();
    dbHandle = undefined;
  });

  it('denies sensitive resources in native RBAC for M3 list reads', async () => {
    const service = makeService();

    await expect(
      service.evaluateReadAccess({
        grantId: 'grant-1',
        peerId: 'peer-1',
        subjectUserId: 'user-1',
        resource: 'credentials',
      }),
    ).resolves.toMatchObject({
      allowed: false,
      reason: 'credentials federation list access is not implemented in M3',
    });
  });

  it('allows personal memory reads without requiring team lookup', async () => {
    const service = makeService();

    await expect(
      service.evaluateReadAccess({
        grantId: 'grant-1',
        peerId: 'peer-1',
        subjectUserId: 'user-1',
        resource: 'memory',
      }),
    ).resolves.toEqual({
      allowed: true,
      access: { includePersonal: true, teamIds: [] },
    });
  });

  it('applies the scope row cap and returns an opaque next cursor when truncated', async () => {
    const service = makeService();
    const listAllRows = stubRows(
      service,
      [
        { id: '3', createdAt: new Date('2026-06-24T03:00:00.000Z') },
        { id: '2', createdAt: new Date('2026-06-24T02:00:00.000Z') },
        { id: '1', createdAt: new Date('2026-06-24T01:00:00.000Z') },
      ],
      [{ id: '1', createdAt: new Date('2026-06-24T01:00:00.000Z') }],
    );

    const firstPage = await service.list({ filter: TASK_FILTER });

    expect(firstPage).toEqual({
      items: [
        { id: '3', createdAt: new Date('2026-06-24T03:00:00.000Z') },
        { id: '2', createdAt: new Date('2026-06-24T02:00:00.000Z') },
      ],
      truncated: true,
      nextCursor: expect.any(String),
    });

    expect(listAllRows).toHaveBeenNthCalledWith(1, TASK_FILTER, 3, undefined);

    const secondPage = await service.list({ filter: TASK_FILTER, cursor: firstPage.nextCursor });
    expect(secondPage).toEqual({
      items: [{ id: '1', createdAt: new Date('2026-06-24T01:00:00.000Z') }],
      truncated: false,
    });
    expect(listAllRows).toHaveBeenNthCalledWith(
      2,
      TASK_FILTER,
      3,
      expect.objectContaining({ id: '2' }),
    );
  });

  it('rejects invalid cursors instead of falling back to the first page', async () => {
    const service = makeService();
    stubRows(service, [{ id: '1' }]);

    await expect(service.list({ filter: TASK_FILTER, cursor: 'not-base64-json' })).rejects.toThrow(
      'Invalid federation list cursor',
    );
  });

  it('throws when a truncated page cannot encode a resumable cursor', async () => {
    const service = makeService();
    stubRows(service, [
      { id: '2', createdAt: 'not-a-date' },
      { id: '1', createdAt: 'not-a-date' },
    ]);

    await expect(service.list({ filter: { ...TASK_FILTER, limit: 1 } })).rejects.toThrow(
      'Federation list cursor cannot be encoded',
    );
  });

  it('throws on unsupported resources instead of crashing pagination', async () => {
    const service = makeService();

    await expect(
      service.list({
        filter: {
          ...TASK_FILTER,
          resource: 'unknown-resource' as FederationScopeQueryFilter['resource'],
        },
      }),
    ).rejects.toThrow('Unsupported federation list resource');
  });

  it('does not leak another user mission task notes through team-scoped note reads', async () => {
    const service = makeDbService();

    const result = await service.list({
      filter: {
        resource: 'notes',
        subjectUserId: SUBJECT_USER_ID,
        includePersonal: false,
        teamIds: [TEAM_ID],
        limit: 10,
        maxRowsPerQuery: 10,
      },
    });

    const ids = result.items.map((item) => item['id']);
    expect(ids).toEqual([SUBJECT_TEAM_NOTE_ID]);
    expect(ids).not.toContain(OTHER_TEAM_NOTE_ID);
  });

  it('does not return subject personal mission task notes when includePersonal is false', async () => {
    const service = makeDbService();

    const result = await service.list({
      filter: {
        resource: 'notes',
        subjectUserId: SUBJECT_USER_ID,
        includePersonal: false,
        teamIds: [TEAM_ID],
        limit: 10,
        maxRowsPerQuery: 10,
      },
    });

    expect(result.items.map((item) => item['id'])).not.toContain(SUBJECT_PERSONAL_NOTE_ID);
  });

  it('does not return subject notes from missions outside the grant-visible project set', async () => {
    const service = makeDbService();

    const result = await service.list({
      filter: {
        resource: 'notes',
        subjectUserId: SUBJECT_USER_ID,
        includePersonal: true,
        teamIds: [TEAM_ID],
        limit: 10,
        maxRowsPerQuery: 10,
      },
    });

    const ids = result.items.map((item) => item['id']);
    expect(ids).toContain(SUBJECT_PERSONAL_NOTE_ID);
    expect(ids).toContain(SUBJECT_TEAM_NOTE_ID);
    expect(ids).not.toContain(SUBJECT_UNAUTHORIZED_NOTE_ID);
    expect(ids).not.toContain(OTHER_TEAM_NOTE_ID);
  });

  it('paginates memory deterministically across insights and preferences', async () => {
    const service = makeDbService();
    const filter: FederationScopeQueryFilter = {
      resource: 'memory',
      subjectUserId: SUBJECT_USER_ID,
      includePersonal: true,
      teamIds: [],
      limit: 2,
      maxRowsPerQuery: 2,
    };

    const firstPage = await service.list({ filter });
    const secondPage = await service.list({ filter, cursor: firstPage.nextCursor });
    const firstPageIds = firstPage.items.map((item) => item['id']);
    const secondPageIds = secondPage.items.map((item) => item['id']);
    const allIds = [...firstPageIds, ...secondPageIds];

    expect(firstPage).toMatchObject({ truncated: true, nextCursor: expect.any(String) });
    expect(firstPageIds).toEqual([INSIGHT_TWO_ID, INSIGHT_ONE_ID]);
    expect(secondPageIds).toEqual([PREFERENCE_TWO_ID, PREFERENCE_ONE_ID]);
    expect(new Set(allIds).size).toBe(allIds.length);
  });
});
