import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
import { seedCompletedDocument, seedDocuments, seedPendingDocument } from '@documenso/prisma/seed/documents';
import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';

import { apiSignin, apiSignout } from '../../fixtures/authentication';

const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();

test.describe.configure({
  mode: 'parallel',
});

const trpcDocumentSearch = async (page: Page, query: string) => {
  const inputParam = encodeURIComponent(JSON.stringify({ json: { query } }));
  const url = `${WEBAPP_BASE_URL}/api/trpc/document.search?input=${inputParam}`;

  const res = await page.context().request.get(url);

  return {
    res,
    data: res.ok()
      ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
        ((await res.json()).result.data.json as Array<{
          title: string;
          path: string;
          value: string;
        }>)
      : null,
  };
};

// ─── Visibility ──────────────────────────────────────────────────────────────

test.describe('Document Search - Visibility', () => {
  test('should respect team document visibility per role', async ({ page }) => {
    const { user: owner, organisation, team } = await seedUser();

    const [adminUser, managerUser, memberUser] = await seedOrganisationMembers({
      organisationId: organisation.id,
      members: [
        { organisationRole: OrganisationMemberRole.ADMIN },
        { organisationRole: OrganisationMemberRole.MEMBER },
        { organisationRole: OrganisationMemberRole.MEMBER },
      ],
    });

    const managerTeamGroup = await prisma.teamGroup.findFirstOrThrow({
      where: { teamId: team.id, teamRole: TeamMemberRole.MANAGER },
      include: { organisationGroup: true },
    });

    const managerOrganisationMember = await prisma.organisationMember.findFirstOrThrow({
      where: { organisationId: organisation.id, userId: managerUser.id },
    });

    await prisma.organisationGroupMember.create({
      data: {
        id: generateDatabaseId('group_member'),
        groupId: managerTeamGroup.organisationGroupId,
        organisationMemberId: managerOrganisationMember.id,
      },
    });

    await seedDocuments([
      {
        sender: owner,
        teamId: team.id,
        recipients: [],
        type: DocumentStatus.COMPLETED,
        documentOptions: {
          visibility: 'EVERYONE',
          title: 'Searchable Document for Everyone',
        },
      },
      {
        sender: owner,
        teamId: team.id,
        recipients: [],
        type: DocumentStatus.COMPLETED,
        documentOptions: {
          visibility: 'MANAGER_AND_ABOVE',
          title: 'Searchable Document for Managers',
        },
      },
      {
        sender: owner,
        teamId: team.id,
        recipients: [],
        type: DocumentStatus.COMPLETED,
        documentOptions: {
          visibility: 'ADMIN',
          title: 'Searchable Document for Admins',
        },
      },
    ]);

    const testCases = [
      { user: adminUser, visibleDocs: 3 },
      { user: managerUser, visibleDocs: 2 },
      { user: memberUser, visibleDocs: 1 },
    ];

    for (const { user, visibleDocs } of testCases) {
      await apiSignin({ page, email: user.email });

      const { data } = await trpcDocumentSearch(page, 'Searchable Document');

      expect(data).not.toBeNull();
      expect(data).toHaveLength(visibleDocs);

      await apiSignout({ page });
    }
  });

  test('should respect visibility when searching by recipient email', async ({ page }) => {
    const { team, owner } = await seedTeam();
    const adminUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.ADMIN });
    const memberUser = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER });

    const { user: uniqueRecipient } = await seedUser();

    await seedDocuments([
      {
        sender: owner,
        recipients: [uniqueRecipient],
        type: DocumentStatus.COMPLETED,
        teamId: team.id,
        documentOptions: {
          visibility: 'ADMIN',
          title: 'Admin Document for Unique Recipient',
        },
      },
    ]);

    // Admin can find the ADMIN-visibility document by recipient email.
    await apiSignin({ page, email: adminUser.email });

    const { data: adminData } = await trpcDocumentSearch(page, uniqueRecipient.email);

    expect(adminData).not.toBeNull();
    expect(adminData).toHaveLength(1);
    expect(adminData![0].title).toBe('Admin Document for Unique Recipient');

    await apiSignout({ page });

    // Member cannot find the ADMIN-visibility document by recipient email.
    await apiSignin({ page, email: memberUser.email });

    const { data: memberData } = await trpcDocumentSearch(page, uniqueRecipient.email);

    expect(memberData).not.toBeNull();
    expect(memberData).toHaveLength(0);

    await apiSignout({ page });
  });
});

// ─── Cross-Team Isolation ────────────────────────────────────────────────────

test.describe('Document Search - Cross-Team Isolation', () => {
  test('should not reveal documents from other teams', async ({ page }) => {
    const { team: teamA, owner: ownerA } = await seedTeam();
    const { team: teamB, owner: ownerB } = await seedTeam();

    const memberA = await seedTeamMember({ teamId: teamA.id, role: TeamMemberRole.MEMBER });

    await seedDocuments([
      {
        sender: ownerA,
        teamId: teamA.id,
        recipients: [],
        type: DocumentStatus.COMPLETED,
        documentOptions: {
          visibility: 'EVERYONE',
          title: 'Unique Team A Document',
        },
      },
      {
        sender: ownerB,
        teamId: teamB.id,
        recipients: [],
        type: DocumentStatus.COMPLETED,
        documentOptions: {
          visibility: 'EVERYONE',
          title: 'Unique Team B Document',
        },
      },
    ]);

    await apiSignin({ page, email: memberA.email });

    const { data } = await trpcDocumentSearch(page, 'Unique');

    expect(data).not.toBeNull();
    const titles = data!.map((d) => d.title);

    expect(titles).toContain('Unique Team A Document');
    expect(titles).not.toContain('Unique Team B Document');

    await apiSignout({ page });
  });

  test('should not cross team boundaries when searching SQL wildcard "%"', async ({ page }) => {
    const { team: teamA, owner: ownerA } = await seedTeam();
    const { team: teamB, owner: ownerB } = await seedTeam();

    await seedDocuments([
      {
        sender: ownerA,
        teamId: teamA.id,
        recipients: [],
        type: DocumentStatus.DRAFT,
        documentOptions: { title: 'Wildcard Doc A' },
      },
    ]);

    await seedDocuments([
      {
        sender: ownerB,
        teamId: teamB.id,
        recipients: [],
        type: DocumentStatus.DRAFT,
        documentOptions: { title: 'Wildcard Doc B' },
      },
    ]);

    await apiSignin({ page, email: ownerA.email });

    const { data } = await trpcDocumentSearch(page, '%');

    expect(data).not.toBeNull();
    expect(data!.map((d) => d.title)).not.toContain('Wildcard Doc B');

    await apiSignout({ page });
  });

  test('should not cross team boundaries when searching SQL wildcard "_"', async ({ page }) => {
    const { team: teamA, owner: ownerA } = await seedTeam();
    const { team: teamB, owner: ownerB } = await seedTeam();

    await seedDocuments([
      {
        sender: ownerA,
        teamId: teamA.id,
        recipients: [],
        type: DocumentStatus.DRAFT,
        documentOptions: { title: 'Underscore A' },
      },
    ]);

    await seedDocuments([
      {
        sender: ownerB,
        teamId: teamB.id,
        recipients: [],
        type: DocumentStatus.DRAFT,
        documentOptions: { title: 'Underscore B' },
      },
    ]);

    await apiSignin({ page, email: ownerA.email });

    const { data } = await trpcDocumentSearch(page, '_');

    expect(data).not.toBeNull();
    expect(data!.map((d) => d.title)).not.toContain('Underscore B');

    await apiSignout({ page });
  });
});

// ─── Recipient Search ────────────────────────────────────────────────────────

test.describe('Document Search - Recipient', () => {
  test('should find documents where user is a recipient', async ({ page }) => {
    const { team: senderTeam, owner: sender } = await seedTeam();
    const { user: recipient } = await seedUser();

    await seedPendingDocument(sender, senderTeam.id, [recipient], {
      createDocumentOptions: { title: 'Recipient Search Test Doc' },
    });

    await apiSignin({ page, email: recipient.email });

    const { data } = await trpcDocumentSearch(page, 'Recipient Search Test');

    expect(data).not.toBeNull();
    expect(data!.length).toBeGreaterThanOrEqual(1);

    const result = data!.find((d) => d.title === 'Recipient Search Test Doc');
    expect(result).toBeDefined();
    expect(result!.path).toMatch(/^\/sign\/.+/);

    await apiSignout({ page });
  });
});

// ─── Token Masking ───────────────────────────────────────────────────────────

test.describe('Document Search - Token Masking', () => {
  test('should not expose signing token in value field', async ({ page }) => {
    const { team, owner } = await seedTeam();
    const { user: recipient } = await seedUser();

    const doc = await seedPendingDocument(owner, team.id, [recipient], {
      createDocumentOptions: { title: 'Token Leakage Test Document' },
    });

    const recipientRecord = await prisma.recipient.findFirstOrThrow({
      where: { envelopeId: doc.id, email: recipient.email },
      select: { token: true },
    });

    const signingToken = recipientRecord.token;

    // Owner should see the doc but not any signing token.
    await apiSignin({ page, email: owner.email });

    const { data } = await trpcDocumentSearch(page, 'Token Leakage Test');

    expect(data).not.toBeNull();
    expect(data!.length).toBeGreaterThanOrEqual(1);

    const result = data!.find((d) => d.title === 'Token Leakage Test Document');
    expect(result).toBeDefined();
    expect(result!.value).not.toContain(signingToken);
    expect(result!.path).not.toContain('/sign/');

    await apiSignout({ page });

    // Recipient should get their own /sign/ path but token still hidden from value.
    await apiSignin({ page, email: recipient.email });

    const { data: recipientData } = await trpcDocumentSearch(page, 'Token Leakage Test');

    expect(recipientData).not.toBeNull();
    expect(recipientData!.length).toBeGreaterThanOrEqual(1);

    const recipientResult = recipientData!.find((d) => d.title === 'Token Leakage Test Document');
    expect(recipientResult).toBeDefined();
    expect(recipientResult!.path).toBe(`/sign/${signingToken}`);
    expect(recipientResult!.value).not.toContain(signingToken);

    await apiSignout({ page });
  });

  test('should not expose other recipients signing tokens to the owner', async ({ page }) => {
    const { team, owner } = await seedTeam();
    const { user: recipientA } = await seedUser();
    const { user: recipientB } = await seedUser();

    const doc = await seedPendingDocument(owner, team.id, [recipientA, recipientB], {
      createDocumentOptions: { title: 'Multi Recipient Token Test' },
    });

    const recipients = await prisma.recipient.findMany({
      where: { envelopeId: doc.id },
      select: { token: true, email: true },
    });

    await apiSignin({ page, email: owner.email });

    const { data } = await trpcDocumentSearch(page, 'Multi Recipient Token');

    expect(data).not.toBeNull();
    const result = data!.find((d) => d.title === 'Multi Recipient Token Test');
    expect(result).toBeDefined();

    for (const r of recipients) {
      expect(result!.value).not.toContain(r.token);
      expect(result!.path).not.toContain(r.token);
    }

    await apiSignout({ page });
  });
});

// ─── Filtering ───────────────────────────────────────────────────────────────

test.describe('Document Search - Filtering', () => {
  test('should exclude soft-deleted documents', async ({ page }) => {
    const { team, owner } = await seedTeam();

    await seedCompletedDocument(owner, team.id, [], {
      createDocumentOptions: { title: 'Active Deletable Document' },
    });

    const deletedDoc = await seedCompletedDocument(owner, team.id, [], {
      createDocumentOptions: { title: 'Deleted Deletable Document' },
    });

    await prisma.envelope.update({
      where: { id: deletedDoc.id },
      data: { deletedAt: new Date() },
    });

    await apiSignin({ page, email: owner.email });

    const { data } = await trpcDocumentSearch(page, 'Deletable Document');

    expect(data).not.toBeNull();
    expect(data).toHaveLength(1);
    expect(data![0].title).toBe('Active Deletable Document');

    await apiSignout({ page });
  });

  test('should find documents by externalId', async ({ page }) => {
    const { team, owner } = await seedTeam();

    await seedDocuments([
      {
        sender: owner,
        teamId: team.id,
        recipients: [],
        type: DocumentStatus.DRAFT,
        documentOptions: {
          title: 'ExternalId Test Doc',
          externalId: 'ext-unique-abc-123',
        },
      },
      {
        sender: owner,
        teamId: team.id,
        recipients: [],
        type: DocumentStatus.DRAFT,
        documentOptions: {
          title: 'Other Test Doc',
          externalId: 'ext-other-xyz',
        },
      },
    ]);

    await apiSignin({ page, email: owner.email });

    const { data } = await trpcDocumentSearch(page, 'ext-unique-abc');

    expect(data).not.toBeNull();
    expect(data).toHaveLength(1);
    expect(data![0].title).toBe('ExternalId Test Doc');

    await apiSignout({ page });
  });
});

// ─── Authentication ──────────────────────────────────────────────────────────

test.describe('Document Search - Authentication', () => {
  test('should reject unauthenticated requests', async ({ page }) => {
    const { res } = await trpcDocumentSearch(page, 'anything');

    expect(res.ok()).toBeFalsy();
    expect(res.status()).toBe(401);
  });
});
