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

const CREDENTIAL_FILTER: FederationScopeQueryFilter = {
  resource: 'credentials',
  subjectUserId: 'user-1',
  includePersonal: true,
  teamIds: [],
  limit: 1,
  maxRowsPerQuery: 25,
};

const SUBJECT_USER_ID = 'fed-m3-06-subject';
const OTHER_USER_ID = 'fed-m3-06-other';
const TEAM_ID = '06000000-0000-4000-8000-000000000001';
const UNAUTHORIZED_TEAM_ID = '06000000-0000-4000-8000-000000000002';
const PERSONAL_PROJECT_ID = '06000000-0000-4000-8000-000000000101';
const TEAM_PROJECT_ID = '06000000-0000-4000-8000-000000000102';
const UNAUTHORIZED_PROJECT_ID = '06000000-0000-4000-8000-000000000103';
const PERSONAL_MISSION_ID = '06000000-0000-4000-8000-000000000201';
const TEAM_MISSION_ID = '06000000-0000-4000-8000-000000000202';
const UNAUTHORIZED_MISSION_ID = '06000000-0000-4000-8000-000000000203';
const SUBJECT_TEAM_NOTE_ID = '06000000-0000-4000-8000-000000000301';
const OTHER_TEAM_NOTE_ID = '06000000-0000-4000-8000-000000000302';
const SUBJECT_PERSONAL_NOTE_ID = '06000000-0000-4000-8000-000000000303';
const SUBJECT_UNAUTHORIZED_NOTE_ID = '06000000-0000-4000-8000-000000000304';

let dbHandle: DbHandle | undefined;

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

function makeDbService() {
  if (!dbHandle) {
    throw new Error('test DB not initialized');
  }
  return new FederationGetQueryService(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-06 Team',
      slug: 'fed-m3-06-team',
      ownerId: SUBJECT_USER_ID,
      managerId: SUBJECT_USER_ID,
    },
    {
      id: UNAUTHORIZED_TEAM_ID,
      name: 'FED-M3-06 Unauthorized Team',
      slug: 'fed-m3-06-unauthorized-team',
      ownerId: OTHER_USER_ID,
      managerId: OTHER_USER_ID,
    },
  ]);

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

  await dbHandle.db.insert(missions).values([
    {
      id: PERSONAL_MISSION_ID,
      name: 'FED-M3-06 Personal Mission',
      projectId: PERSONAL_PROJECT_ID,
      userId: SUBJECT_USER_ID,
    },
    {
      id: TEAM_MISSION_ID,
      name: 'FED-M3-06 Team Mission',
      projectId: TEAM_PROJECT_ID,
      userId: SUBJECT_USER_ID,
    },
    {
      id: UNAUTHORIZED_MISSION_ID,
      name: 'FED-M3-06 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'),
    },
  ]);
}

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

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

  it('denies sensitive resources in native RBAC for M3 get 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 get 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('uses subject team membership as the native RBAC upper bound for task and note reads', async () => {
    const service = makeService();
    const listSubjectTeamIds = vi.fn().mockResolvedValue(['team-1', 'team-2']);
    (
      service as unknown as {
        listSubjectTeamIds: (subjectUserId: string) => Promise<string[]>;
      }
    ).listSubjectTeamIds = listSubjectTeamIds;

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

  it('does not query storage for sensitive get resources even if scope allowed them', async () => {
    const service = makeService();

    await expect(service.get({ filter: CREDENTIAL_FILTER, id: 'cred-1' })).resolves.toEqual({
      status: 'denied',
      reason: 'credentials federation get is not implemented',
    });
  });

  it('fails closed for unsupported resources instead of returning undefined', async () => {
    const service = makeService();

    await expect(
      service.get({
        filter: {
          ...CREDENTIAL_FILTER,
          resource: 'unknown-resource' as FederationScopeQueryFilter['resource'],
        },
        id: 'row-1',
      }),
    ).resolves.toEqual({
      status: 'denied',
      reason: 'Unsupported federation get resource: unknown-resource',
    });
  });

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

    await expect(
      service.get({
        filter: {
          resource: 'notes',
          subjectUserId: SUBJECT_USER_ID,
          includePersonal: false,
          teamIds: [TEAM_ID],
          limit: 1,
          maxRowsPerQuery: 10,
        },
        id: OTHER_TEAM_NOTE_ID,
      }),
    ).resolves.toEqual({
      status: 'denied',
      reason: 'Note is outside the federated scope',
    });
  });

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

    await expect(
      service.get({
        filter: {
          resource: 'notes',
          subjectUserId: SUBJECT_USER_ID,
          includePersonal: true,
          teamIds: [TEAM_ID],
          limit: 1,
          maxRowsPerQuery: 10,
        },
        id: SUBJECT_UNAUTHORIZED_NOTE_ID,
      }),
    ).resolves.toEqual({
      status: 'denied',
      reason: 'Note is outside the federated scope',
    });
  });

  it('returns a subject note only when subject ownership and authorized mission intersect', async () => {
    const service = makeDbService();

    await expect(
      service.get({
        filter: {
          resource: 'notes',
          subjectUserId: SUBJECT_USER_ID,
          includePersonal: false,
          teamIds: [TEAM_ID],
          limit: 1,
          maxRowsPerQuery: 10,
        },
        id: SUBJECT_TEAM_NOTE_ID,
      }),
    ).resolves.toMatchObject({
      status: 'found',
      item: {
        id: SUBJECT_TEAM_NOTE_ID,
        missionId: TEAM_MISSION_ID,
        content: 'subject note on team mission',
      },
    });
  });

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

    await expect(
      service.get({
        filter: {
          resource: 'notes',
          subjectUserId: SUBJECT_USER_ID,
          includePersonal: false,
          teamIds: [TEAM_ID],
          limit: 1,
          maxRowsPerQuery: 10,
        },
        id: SUBJECT_PERSONAL_NOTE_ID,
      }),
    ).resolves.toEqual({
      status: 'denied',
      reason: 'Note is outside the federated scope',
    });
  });
});
