import { Pool } from 'pg';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { connectTestDb } from '../../../../__tests__/helpers/db-connection.js';
import { runMigrationsIfNeeded } from '../../../../__tests__/helpers/db-migrations.js';
import { dropRlsRole, ensureRlsRole, runAsRlsRole } from '../../../../__tests__/helpers/rls-role.js';
import type { AuthContextProvider } from '../../../auth/providers/auth-context.provider.js';
import { DBProvider } from '../../../app-providers/db.provider.js';
import { TenantAwareDBClient } from '../../../app-providers/tenant-db-client.js';
import type { FinancialAccountsProvider } from '../../../financial-accounts/providers/financial-accounts.provider.js';
import type { FinancialBankAccountsProvider } from '../../../financial-accounts/providers/financial-bank-accounts.provider.js';
import type { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js';
import { ForeignSecuritiesProvider } from '../foreign-securities.provider.js';
import { SecurityBusinessesProvider } from '../security-businesses.provider.js';

let pool: Pool;

// poalim_securities.owner_id is a NOT NULL FK, so the tenants this suite acts as must
// exist in accounter_schema.businesses. Seeded in beforeAll, removed in afterAll.
const TEST_OWNER_ID = '00000000-0000-0000-0000-0000000006ec';
const OTHER_OWNER_ID = '00000000-0000-0000-0000-0000000006ed';

const CHARGE_ID = '00000000-0000-0000-0000-00000000c001';

const ACCOUNT_ID = '00000000-0000-0000-0000-00000000a001';

// The synthetic security businesses. Fixed ids so cleanup and assertions are deterministic.
const APPLE_BUSINESS_ID = '00000000-0000-0000-0000-0000000005a1';
const MSFT_BUSINESS_ID = '00000000-0000-0000-0000-0000000005a2';
const ISIN_ONLY_BUSINESS_ID = '00000000-0000-0000-0000-0000000005a3';
const SECURITY_BUSINESS_IDS = [APPLE_BUSINESS_ID, MSFT_BUSINESS_ID, ISIN_ONLY_BUSINESS_ID];
const BANK_NUMBER = 12;
const BRANCH_NUMBER = 615;
const ACCOUNT_NUMBER = 100000;

function createMockAuthContextProvider(businessId: string): AuthContextProvider {
  return {
    getAuthContext: () =>
      Promise.resolve({
        authType: 'apiKey' as const,
        token: 'test-token',
        tenant: { businessId },
        user: {
          userId: 'api-key:test',
          auth0UserId: null,
          email: '',
          roleId: 'admin',
          permissions: [],
          emailVerified: true,
          permissionsVersion: 0,
        },
      }),
  } as unknown as AuthContextProvider;
}

type StubTransaction = {
  id: string;
  source_description: string | null;
  amount?: string;
  currency?: string;
  debit_date?: Date | null;
  debit_date_override?: Date | null;
  account_id?: string;
};

/** The day the fixtures settle on: execution value date and transaction debit date alike. */
const VALUE_DATE = '2024-03-12';

/**
 * The provider reads only the identity, description, account and date/amount fields off each
 * transaction. Stubbing the loader keeps these cases focused on the DB-dependent behaviour
 * (RLS scoping, the freshest-row pick, execution matching) instead of seeding the
 * financial_accounts → transactions_raw_list → transactions FK chain.
 */
function createStubTransactionsProvider(transactions: StubTransaction[]): TransactionsProvider {
  return {
    transactionsByChargeIDLoader: {
      load: (chargeId: string) =>
        Promise.resolve(
          transactions.map(transaction => ({
            charge_id: chargeId,
            amount: '-1000.00',
            currency: 'USD',
            debit_date: new Date(`${VALUE_DATE}T00:00:00`),
            debit_date_override: null,
            account_id: ACCOUNT_ID,
            ...transaction,
          })),
        ),
    },
  } as unknown as TransactionsProvider;
}

/**
 * The account lookups are pure id → row maps in the real providers; stubbing them keeps this
 * suite off the financial_accounts fixtures while still exercising the tuple resolution
 * (text account_number → integer) the provider does.
 */
function createStubFinancialAccountsProvider(accountNumber = String(ACCOUNT_NUMBER)) {
  return {
    getFinancialAccountByAccountIDLoader: {
      load: (id: string) =>
        Promise.resolve(id === ACCOUNT_ID ? { id, account_number: accountNumber } : undefined),
    },
  } as unknown as FinancialAccountsProvider;
}

function createStubFinancialBankAccountsProvider() {
  return {
    getFinancialBankAccountByIdLoader: {
      load: (id: string) =>
        Promise.resolve(
          id === ACCOUNT_ID
            ? { id, bank_number: BANK_NUMBER, branch_number: BRANCH_NUMBER }
            : undefined,
        ),
    },
  } as unknown as FinancialBankAccountsProvider;
}

function createProvider(
  transactions: StubTransaction[],
  businessId = TEST_OWNER_ID,
  accountNumber?: string,
) {
  const authContextProvider = createMockAuthContextProvider(businessId);
  const dbClient = new TenantAwareDBClient(new DBProvider(pool), authContextProvider);
  return new ForeignSecuritiesProvider(
    dbClient,
    createStubTransactionsProvider(transactions),
    createStubFinancialAccountsProvider(accountNumber),
    createStubFinancialBankAccountsProvider(),
    // The holdings path reads security businesses and their identifiers, so this one has to be
    // real. Only `db` is exercised: the four remaining constructor deps drive the create path,
    // which these cases seed around by inserting the rows directly.
    new SecurityBusinessesProvider(
      dbClient,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
    ),
  );
}

type SecurityFixture = {
  ownerId?: string;
  branchNumber?: number;
  accountNumber?: number;
  securityKey: string;
  engName?: string;
  asOfDate?: string;
};

// Synthetic values only — never lifted from a real bank capture.
async function insertSecurity({
  ownerId = TEST_OWNER_ID,
  branchNumber = 615,
  accountNumber = 100000,
  securityKey,
  engName = 'Example Corp',
  asOfDate = '2024-01-15T10:00:00.000+02:00',
}: SecurityFixture) {
  await pool.query(
    `INSERT INTO accounter_schema.poalim_securities (
       owner_id, bank_number, branch_number, account_number, as_of_date, security_key,
       eng_name, heb_name, item_type, is_etf, is_foreign, currency_code, exchange,
       equity_type, equity_sub_type, eng_symbol, heb_symbol, symbol, stock_type
     ) VALUES ($1, 12, $2, $3, $4, $5, $6, 'אקזמפל', 'Equity', false, true, 'USD', 'NYQ',
               1, 1, 'EXMP', 'EXMP', 'EXMP', 'Equity')`,
    [ownerId, branchNumber, accountNumber, asOfDate, securityKey, engName],
  );
}

type ExecutionFixture = {
  ownerId?: string;
  branchNumber?: number;
  accountNumber?: number;
  security: string;
  tradeDate?: string;
  valueDate?: string | null;
  tradeType?: string;
  netValueTradeCurrency?: string;
  /** Units moved. Fractional on purpose in the holdings cases — ETFs trade that way. */
  nv?: string;
};

// Synthetic values only — never lifted from a real bank capture.
async function insertExecution({
  ownerId = TEST_OWNER_ID,
  branchNumber = BRANCH_NUMBER,
  accountNumber = ACCOUNT_NUMBER,
  security,
  tradeDate = '2024-03-10',
  valueDate = VALUE_DATE,
  tradeType = 'קניה',
  netValueTradeCurrency = '1000.00',
  nv = '10',
}: ExecutionFixture) {
  // trade_type/transaction_type carry the bank's own Hebrew vocabulary; on a plain buy or sale
  // the two agree (an invariant the scraper's zod schema enforces).
  await pool.query(
    `INSERT INTO accounter_schema.poalim_securities_transactions (
       owner_id, bank_number, branch_number, account_number, security, trade_date, value_date,
       trade_type, transaction_type, nv, trade_price, net_value_trade_currency, trade_currency
     ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8, $10, 100, $9, 'דולר ארה"ב')`,
    [
      ownerId,
      BANK_NUMBER,
      branchNumber,
      accountNumber,
      security,
      tradeDate,
      valueDate,
      tradeType,
      netValueTradeCurrency,
      nv,
    ],
  );
}

/**
 * A security business and the Poalim keys it answers to, inserted directly rather than through
 * `SecurityBusinessesProvider.ensureSecurityBusiness` — that path needs an admin context and a
 * general foreign-securities business, neither of which the holdings query reads.
 */
async function insertSecurityBusiness({
  id,
  isin,
  engName = 'Example Corp',
  securityKeys = [],
  ownerId = TEST_OWNER_ID,
}: {
  id: string;
  isin: string;
  engName?: string;
  securityKeys?: string[];
  ownerId?: string;
}) {
  await pool.query(
    `INSERT INTO accounter_schema.financial_entities (id, owner_id, name, type)
     VALUES ($1, $2, $3, 'business')`,
    [id, ownerId, `${engName} (${isin})`],
  );
  await pool.query(
    `INSERT INTO accounter_schema.businesses (id, owner_id) VALUES ($1, $2)`,
    [id, ownerId],
  );
  await pool.query(
    `INSERT INTO accounter_schema.businesses_securities (id, owner_id, isin, eng_name, symbol)
     VALUES ($1, $2, $3, $4, 'EXMP')`,
    [id, ownerId, isin, engName],
  );
  for (const securityKey of securityKeys) {
    await pool.query(
      `INSERT INTO accounter_schema.security_identifiers
         (owner_id, business_id, identifier_type, identifier_value)
       VALUES ($1, $2, 'POALIM_SECURITY_KEY', $3)`,
      [ownerId, id, securityKey],
    );
  }
}

beforeAll(async () => {
  pool = await connectTestDb();
  await runMigrationsIfNeeded(pool);

  for (const [index, ownerId] of [TEST_OWNER_ID, OTHER_OWNER_ID].entries()) {
    await pool.query(
      `INSERT INTO accounter_schema.financial_entities (id, name)
       VALUES ($1, $2)
       ON CONFLICT (id) DO NOTHING`,
      [ownerId, `foreign-securities-test-owner-${index}`],
    );
    await pool.query(
      `INSERT INTO accounter_schema.businesses (id, owner_id)
       VALUES ($1, $1)
       ON CONFLICT (id) DO NOTHING`,
      [ownerId],
    );
  }

  await ensureRlsRole(pool, {
    grants: [
      { table: 'poalim_securities', privileges: 'SELECT' },
      { table: 'poalim_securities_transactions', privileges: 'SELECT' },
      { table: 'businesses_securities', privileges: 'SELECT' },
      { table: 'security_identifiers', privileges: 'SELECT' },
    ],
  });
});

afterAll(async () => {
  await dropRlsRole(pool);
  await deleteSecurityBusinesses();
  // poalim_securities and poalim_securities_transactions rows cascade with the owning business.
  for (const ownerId of [TEST_OWNER_ID, OTHER_OWNER_ID]) {
    await pool.query('DELETE FROM accounter_schema.businesses WHERE id = $1', [ownerId]);
    await pool.query('DELETE FROM accounter_schema.financial_entities WHERE id = $1', [ownerId]);
  }
  // Do NOT close the pool here — it is shared with other concurrently-running suites and
  // torn down by vitest-global-setup.
});

// DELETE rather than TRUNCATE CASCADE: TRUNCATE takes ACCESS EXCLUSIVE locks that deadlock
// against concurrently-running integration suites.
// Scoped to this suite's owners: the poalim_* tables are shared with the concurrently-running
// scraper-ingestion suite, and an unqualified DELETE wipes its fixtures mid-test.
beforeEach(async () => {
  const owners = [TEST_OWNER_ID, OTHER_OWNER_ID];
  await pool.query('DELETE FROM accounter_schema.poalim_securities WHERE owner_id = ANY($1)', [
    owners,
  ]);
  await pool.query(
    'DELETE FROM accounter_schema.poalim_securities_transactions WHERE owner_id = ANY($1)',
    [owners],
  );
  await deleteSecurityBusinesses();
});

/**
 * The synthetic security businesses, by explicit id so the tenants' own business rows survive
 * between tests. `businesses` first: its FK to `financial_entities` does not cascade, while
 * businesses_securities and security_identifiers do cascade from `businesses`.
 */
async function deleteSecurityBusinesses() {
  await pool.query('DELETE FROM accounter_schema.businesses WHERE id = ANY($1)', [
    SECURITY_BUSINESS_IDS,
  ]);
  await pool.query('DELETE FROM accounter_schema.financial_entities WHERE id = ANY($1)', [
    SECURITY_BUSINESS_IDS,
  ]);
}

describe('getChargeSecurities', () => {
  it('resolves a key from a transaction description to its ingested security', async () => {
    await insertSecurity({ securityKey: '5129523', engName: 'Example Corp' });
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז מכירה 0005129523' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities).toHaveLength(1);
    expect(securities[0].securityKey).toBe('5129523');
    expect(securities[0].id).toBe(`${CHARGE_ID}-5129523`);
    expect(securities[0].details?.eng_name).toBe('Example Corp');
    expect(securities[0].transactionIds).toEqual(['t1']);
  });

  it('returns an unresolved entry when the key has no ingested row', async () => {
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז קניה 0077774297' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities).toHaveLength(1);
    expect(securities[0].securityKey).toBe('77774297');
    expect(securities[0].details).toBeNull();
    expect(securities[0].transactionIds).toEqual(['t1']);
  });

  it('groups every transaction carrying the same key under one entry', async () => {
    await insertSecurity({ securityKey: '5129523' });
    const provider = createProvider([
      { id: 'trade', source_description: 'ניע"ז קניה 0005129523' },
      { id: 'fee', source_description: 'ניע"ז עמ קניה 0005129523' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities).toHaveLength(1);
    expect(securities[0].transactionIds).toEqual(['trade', 'fee']);
  });

  it('returns an empty array when no description carries a key', async () => {
    await insertSecurity({ securityKey: '5129523' });
    const provider = createProvider([
      { id: 't1', source_description: 'ניעז עמ תשלום fsec pymnt fee' },
      { id: 't2', source_description: null },
    ]);

    expect(await provider.getChargeSecurities(CHARGE_ID)).toEqual([]);
  });

  it('returns one entry per distinct key on a merged charge', async () => {
    await insertSecurity({ securityKey: '5129523', engName: 'Example Corp' });
    await insertSecurity({
      securityKey: '77774297',
      engName: 'Other Corp',
      accountNumber: 100001,
    });
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז מכירה 0005129523' },
      { id: 't2', source_description: 'ניע"ז קניה 0077774297' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities.map(s => s.securityKey)).toEqual(['5129523', '77774297']);
    expect(securities.map(s => s.details?.eng_name)).toEqual(['Example Corp', 'Other Corp']);
  });

  it('picks the freshest row when one tenant holds the key in several accounts', async () => {
    // The dedup index is (owner_id, bank, branch, account, key), so this is reachable.
    await insertSecurity({
      securityKey: '5129523',
      accountNumber: 100000,
      engName: 'Stale Name',
      asOfDate: '2024-01-15T10:00:00.000+02:00',
    });
    await insertSecurity({
      securityKey: '5129523',
      accountNumber: 100001,
      engName: 'Fresh Name',
      asOfDate: '2024-06-30T10:00:00.000+02:00',
    });
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז מכירה 0005129523' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities).toHaveLength(1);
    expect(securities[0].details?.eng_name).toBe('Fresh Name');
  });

  /**
   * The provider's query carries no owner_id predicate — RLS is what scopes it. The test
   * pool connects as postgres (BYPASSRLS), so asserting that through the provider would
   * prove nothing; this runs the same query shape under a non-superuser role, which is
   * the boundary the server actually operates behind.
   */
  it("does not expose another tenant's securities under the tenant_isolation policy", async () => {
    await insertSecurity({ ownerId: OTHER_OWNER_ID, securityKey: '5129523' });

    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      // Session variables must be set as superuser, before privileges are dropped.
      await client.query(`SELECT set_config('app.current_business_id', $1, true)`, [TEST_OWNER_ID]);

      const rows = await runAsRlsRole(client, async () => {
        const result = await client.query(
          `SELECT security_key FROM accounter_schema.poalim_securities
           WHERE security_key = ANY($1)`,
          [['5129523']],
        );
        return result.rows;
      });

      expect(rows).toEqual([]);
    } finally {
      await client.query('ROLLBACK');
      client.release();
    }
  });
});

describe('getChargeSecurities — matched executions', () => {
  it('attaches the execution behind the charge transaction', async () => {
    await insertSecurity({ securityKey: '5129523' });
    await insertExecution({ security: '5129523', netValueTradeCurrency: '1000.00' });
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז קניה 0005129523', amount: '-1000.00' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities[0].executions).toHaveLength(1);
    expect(securities[0].executions[0].trade_type).toBe('קניה');
    expect(securities[0].executions[0].net_value_trade_currency).toBe('1000.00');
  });

  it('picks the same-day execution whose amount matches the transaction', async () => {
    await insertSecurity({ securityKey: '5129523' });
    await insertExecution({ security: '5129523', netValueTradeCurrency: '1000.00' });
    await insertExecution({
      security: '5129523',
      netValueTradeCurrency: '2500.50',
      tradeType: 'מכירה',
    });
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז מכירה 0005129523', amount: '2500.50' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities[0].executions.map(e => e.net_value_trade_currency)).toEqual(['2500.50']);
  });

  it('excludes an execution booked in another account', async () => {
    await insertSecurity({ securityKey: '5129523' });
    await insertExecution({ security: '5129523', accountNumber: 100001 });
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז קניה 0005129523', amount: '-1000.00' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities[0].executions).toEqual([]);
  });

  it('excludes an execution whose amount does not match', async () => {
    await insertSecurity({ securityKey: '5129523' });
    await insertExecution({ security: '5129523', netValueTradeCurrency: '4321.00' });
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז קניה 0005129523', amount: '-1000.00' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities[0].executions).toEqual([]);
  });

  it('excludes an execution that settles on another day', async () => {
    await insertSecurity({ securityKey: '5129523' });
    await insertExecution({ security: '5129523', valueDate: '2024-01-05' });
    const provider = createProvider([
      { id: 't1', source_description: 'ניע"ז קניה 0005129523', amount: '-1000.00' },
    ]);

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities[0].executions).toEqual([]);
  });

  it('returns no executions when the account number is not a Poalim integer', async () => {
    await insertSecurity({ securityKey: '5129523' });
    await insertExecution({ security: '5129523' });
    const provider = createProvider(
      [{ id: 't1', source_description: 'ניע"ז קניה 0005129523', amount: '-1000.00' }],
      TEST_OWNER_ID,
      'IL12-3456',
    );

    const securities = await provider.getChargeSecurities(CHARGE_ID);

    expect(securities[0].executions).toEqual([]);
  });

  it("does not expose another tenant's executions under the tenant_isolation policy", async () => {
    await insertExecution({ ownerId: OTHER_OWNER_ID, security: '5129523' });

    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      await client.query(`SELECT set_config('app.current_business_id', $1, true)`, [TEST_OWNER_ID]);

      const rows = await runAsRlsRole(client, async () => {
        const result = await client.query(
          `SELECT security FROM accounter_schema.poalim_securities_transactions
           WHERE security = ANY($1)`,
          [['5129523']],
        );
        return result.rows;
      });

      expect(rows).toEqual([]);
    } finally {
      await client.query('ROLLBACK');
      client.release();
    }
  });
});

describe('getExecutionsBySecurityBusiness', () => {
  it('gives every security business its own executions, chronologically', async () => {
    await insertSecurityBusiness({
      id: APPLE_BUSINESS_ID,
      isin: 'US0378331005',
      engName: 'APPLE INC',
      securityKeys: ['1097'],
    });
    await insertSecurityBusiness({
      id: MSFT_BUSINESS_ID,
      isin: 'US5949181045',
      engName: 'MICROSOFT CORP',
      securityKeys: ['2044'],
    });
    await insertExecution({ security: '1097', tradeDate: '2024-03-10' });
    await insertExecution({ security: '1097', tradeDate: '2024-01-05' });
    await insertExecution({ security: '2044', tradeDate: '2024-02-02' });

    const executionsByBusinessId = await createProvider([]).getExecutionsBySecurityBusiness();

    expect(executionsByBusinessId.get(APPLE_BUSINESS_ID)).toHaveLength(2);
    // The SQL orders globally by trade_date, so each business's slice stays chronological —
    // which is what the position derivation reads its currency and start date off.
    expect(
      executionsByBusinessId.get(APPLE_BUSINESS_ID)?.map(execution => execution.trade_date),
    ).toEqual([new Date('2024-01-05T00:00:00'), new Date('2024-03-10T00:00:00')]);
    expect(executionsByBusinessId.get(MSFT_BUSINESS_ID)).toHaveLength(1);
  });

  it('collapses several Poalim keys onto the one security business they name', async () => {
    await insertSecurityBusiness({
      id: APPLE_BUSINESS_ID,
      isin: 'US0378331005',
      securityKeys: ['1097', '1098'],
    });
    await insertExecution({ security: '1097' });
    await insertExecution({ security: '1098' });

    const executionsByBusinessId = await createProvider([]).getExecutionsBySecurityBusiness();

    expect(executionsByBusinessId.get(APPLE_BUSINESS_ID)).toHaveLength(2);
  });

  it('keeps a security business with no Poalim key, with nothing against it', async () => {
    // Its ISIN was ingested but no execution ever named it by key. It is still a security the
    // tenant has, so it has to stay listable — a zero position, not a missing row.
    await insertSecurityBusiness({
      id: ISIN_ONLY_BUSINESS_ID,
      isin: 'IL0010811243',
      securityKeys: [],
    });

    const executionsByBusinessId = await createProvider([]).getExecutionsBySecurityBusiness();

    expect(executionsByBusinessId.has(ISIN_ONLY_BUSINESS_ID)).toBe(true);
    expect(executionsByBusinessId.get(ISIN_ONLY_BUSINESS_ID)).toEqual([]);
  });

  it('ignores executions whose key belongs to no security business', async () => {
    await insertSecurityBusiness({
      id: APPLE_BUSINESS_ID,
      isin: 'US0378331005',
      securityKeys: ['1097'],
    });
    await insertExecution({ security: '1097' });
    await insertExecution({ security: '9999' });

    const executionsByBusinessId = await createProvider([]).getExecutionsBySecurityBusiness();

    expect(executionsByBusinessId.size).toBe(1);
    expect(executionsByBusinessId.get(APPLE_BUSINESS_ID)).toHaveLength(1);
  });

  it('has no entries at all when the tenant has no security businesses', async () => {
    await insertExecution({ security: '1097' });

    const executionsByBusinessId = await createProvider([]).getExecutionsBySecurityBusiness();

    expect(executionsByBusinessId.size).toBe(0);
  });

  /**
   * As with the poalim_* tables above, a provider-level assertion proves nothing here: the
   * suite connects as a superuser, who bypasses RLS. This runs the query shape the holdings
   * path depends on under the non-superuser role the server actually operates behind.
   */
  it("does not expose another tenant's security businesses under tenant_isolation", async () => {
    await insertSecurityBusiness({
      id: APPLE_BUSINESS_ID,
      isin: 'US0378331005',
      securityKeys: ['1097'],
      ownerId: OTHER_OWNER_ID,
    });

    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      // Session variables must be set as superuser, before privileges are dropped.
      await client.query(`SELECT set_config('app.current_business_id', $1, true)`, [TEST_OWNER_ID]);

      const rows = await runAsRlsRole(client, async () => {
        const result = await client.query(
          `SELECT bs.isin, si.identifier_value
           FROM accounter_schema.businesses_securities bs
           LEFT JOIN accounter_schema.security_identifiers si ON si.business_id = bs.id`,
        );
        return result.rows;
      });

      expect(rows).toEqual([]);
    } finally {
      await client.query('ROLLBACK');
      client.release();
    }
  });
});
