/**
 * P0-4 smoke test — AI chat tools enforce ToolContext.
 *
 * Run: npx tsx scripts/smoke/p0-4-ai-scope.ts
 *
 * Offline-safe: exercises tool functions with a stubbed Prisma client so
 * we can prove the scoping logic directly. No real DB / network access.
 */
import { ok, fail } from './_lib';

// Stub the prisma import the tools use. We hijack the module cache before
// the tools module is loaded.
type Spy = (...args: unknown[]) => unknown;
const calls: Record<string, unknown[][]> = {};
function recordCall(name: string, args: unknown[]): void {
    calls[name] = calls[name] ?? [];
    calls[name].push(args);
}
function spyResolves<T>(name: string, value: T): Spy {
    return (...args: unknown[]) => {
        recordCall(name, args);
        return Promise.resolve(value);
    };
}

// We can't easily mock @/lib/prisma without jest's module registry. Instead,
// load the modules through Node's normal resolution and prove tenant scoping
// by inspecting argument capture via a wrapper.
//
// Approach: import tools.ts, then build context objects and assert the
// public contract — refusal without context, and admin-vs-viewer behavior
// on the in-process Prisma client. Even if the real DB is unreachable, the
// assertContext() check fires synchronously before any DB call, so the
// "refusal" cases are always exercisable. The "filter included" cases are
// asserted by reading the function source and checking the contract test
// suite (already run via jest), and by exercising the runtime path against
// an in-memory monkey-patched prisma.

const aiTools = require('../../src/lib/ai/tools') as typeof import('../../src/lib/ai/tools');
const prismaMod = require('../../src/lib/prisma') as { prisma: Record<string, unknown> };

// Monkey-patch the prisma client used by the tools module to capture WHERE
// clauses without hitting the database.
prismaMod.prisma.monitor = {
    findMany: spyResolves('monitor.findMany', []),
    count: spyResolves('monitor.count', 0),
} as unknown;
prismaMod.prisma.statusIncident = {
    findMany: spyResolves('statusIncident.findMany', []),
} as unknown;
prismaMod.prisma.heartbeat = {
    findFirst: spyResolves('heartbeat.findFirst', null),
} as unknown;
prismaMod.prisma.heartbeatHourly = {
    groupBy: spyResolves('heartbeatHourly.groupBy', []),
} as unknown;
prismaMod.prisma.country = {
    findFirst: spyResolves('country.findFirst', null),
    findMany: spyResolves('country.findMany', []),
} as unknown;
prismaMod.prisma.user = {
    count: spyResolves('user.count', 0),
} as unknown;
prismaMod.prisma.chatMessage = {
    findMany: spyResolves('chatMessage.findMany', []),
} as unknown;

const viewer = { userId: 42, role: 'VIEWER', userEmail: 'viewer@example.com', sessionId: 7 };
const admin = { userId: 1, role: 'ADMIN', userEmail: 'admin@example.com', sessionId: 7 };

async function main(): Promise<void> {
    // 1. Refusal without context.
    try {
        // @ts-expect-error - intentional missing ctx
        await aiTools.getMonitors({});
        fail('P0-4 refusal', 'expected getMonitors to throw without ctx');
    } catch (err) {
        if (String(err).includes('ToolContext')) ok('getMonitors refuses without ctx');
        else fail('P0-4 refusal', `wrong error: ${String(err)}`);
    }

    // 2. VIEWER calling getMonitors injects userId into where clause.
    delete calls['monitor.findMany'];
    await aiTools.getMonitors({}, viewer);
    const findManyCall = calls['monitor.findMany']?.[0]?.[0] as { where: { userId?: number; deletedAt?: unknown } } | undefined;
    if (!findManyCall) fail('P0-4 viewer scope', 'monitor.findMany was not called');
    if (findManyCall!.where.userId !== 42) {
        fail('P0-4 viewer scope', `expected userId=42, got ${JSON.stringify(findManyCall!.where)}`);
    }
    ok('VIEWER calling getMonitors restricts WHERE.userId to 42');

    // 3. ADMIN calling getMonitors omits userId.
    delete calls['monitor.findMany'];
    await aiTools.getMonitors({}, admin);
    const adminFind = calls['monitor.findMany']?.[0]?.[0] as { where: { userId?: number } };
    if (adminFind.where.userId !== undefined) {
        fail('P0-4 admin scope', `expected no userId filter, got userId=${adminFind.where.userId}`);
    }
    ok('ADMIN calling getMonitors omits userId filter');

    // 4. VIEWER cannot run getUserStats.
    const userStats = await aiTools.getUserStats({}, viewer);
    if ((userStats as { error?: string }).error !== 'forbidden') {
        fail('P0-4 admin-only', `getUserStats(VIEWER) expected forbidden, got ${JSON.stringify(userStats)}`);
    }
    ok('VIEWER calling getUserStats returns forbidden');

    // 5. VIEWER cannot run getTicketStats.
    const ticketStats = await aiTools.getTicketStats({}, viewer);
    if ((ticketStats as { error?: string }).error !== 'forbidden') {
        fail('P0-4 admin-only', `getTicketStats(VIEWER) expected forbidden, got ${JSON.stringify(ticketStats)}`);
    }
    ok('VIEWER calling getTicketStats returns forbidden');

    // 6. sendDataReport ignores targetEmail and would route to ctx.userEmail.
    //    We capture the email recipient via a stubbed EmailService import.
    const emailModule = require('../../src/lib/services/email.service') as {
        EmailService: { sendRaw: (...args: unknown[]) => Promise<{ success: boolean }> };
    };
    let capturedRecipient = '';
    emailModule.EmailService.sendRaw = async (recipient: unknown) => {
        capturedRecipient = String(recipient);
        return { success: true };
    };
    await aiTools.sendDataReport(
        // The LLM might smuggle a rogue targetEmail; assert it's ignored.
        { reportType: 'monitors', targetEmail: 'attacker@evil.com' } as never,
        viewer
    );
    if (capturedRecipient !== 'viewer@example.com') {
        fail(
            'P0-4 email scope',
            `expected viewer@example.com, got ${capturedRecipient}`
        );
    }
    ok('sendDataReport routes to ctx.userEmail regardless of targetEmail arg');

    ok('P0-4 ai-scope', 'all tenant-scope guarantees hold');
}

main().catch((err) => fail('P0-4 ai-scope', String(err)));
