import { csvEscape, cefEscape, entryToCsvRow, entryToCefLine, AuditExportService } from '../audit-export.service';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';

describe('csvEscape', () => {
    it('passes simple values through', () => {
        expect(csvEscape('hello')).toBe('hello');
        expect(csvEscape(42)).toBe('42');
    });
    it('returns empty for null / undefined', () => {
        expect(csvEscape(null)).toBe('');
        expect(csvEscape(undefined)).toBe('');
    });
    it('quotes values containing comma / quote / CR / LF', () => {
        expect(csvEscape('a,b')).toBe('"a,b"');
        expect(csvEscape('he said "hi"')).toBe('"he said ""hi"""');
        expect(csvEscape('line1\nline2')).toBe('"line1\nline2"');
    });
});

describe('cefEscape', () => {
    it('escapes backslash, equals, pipe', () => {
        expect(cefEscape('a=b|c\\d')).toBe('a\\=b\\|c\\\\d');
    });
    it('replaces newlines with spaces (CEF requires single-line extension values)', () => {
        expect(cefEscape('line1\nline2')).toBe('line1 line2');
    });
    it('returns empty for null', () => {
        expect(cefEscape(null)).toBe('');
    });
});

const sampleEntry = {
    id: 7,
    createdAt: new Date('2026-05-22T10:30:00Z'),
    action: 'DELETE',
    resource: 'Monitor',
    resourceId: '42',
    details: 'admin removed Monitor 42',
    user: { email: 'admin@example.com', name: 'Admin' },
};

describe('entryToCsvRow', () => {
    it('produces a comma-separated row with the expected column order', () => {
        const row = entryToCsvRow(sampleEntry);
        expect(row).toBe('7,2026-05-22T10:30:00.000Z,admin@example.com,DELETE,Monitor,42,admin removed Monitor 42');
    });
    it('quotes details that contain commas', () => {
        const row = entryToCsvRow({ ...sampleEntry, details: 'a, b, c' });
        expect(row).toContain('"a, b, c"');
    });
});

describe('entryToCefLine', () => {
    it('produces a CEF:0 header + extension fields', () => {
        const line = entryToCefLine(sampleEntry);
        expect(line.startsWith('CEF:0|EvidenceAction|UptimeSentinel|1.0|DELETE|')).toBe(true);
        expect(line).toContain('act=DELETE');
        expect(line).toContain('cs1=Monitor');
        expect(line).toContain('cs2=42');
        expect(line).toContain('externalId=7');
        expect(line).toContain('suser=admin@example.com');
    });
    it('escapes pipes inside extension values so they don\'t terminate fields', () => {
        const line = entryToCefLine({ ...sampleEntry, details: 'a|b' });
        expect(line).toContain('msg=a\\|b');
    });
});

describe('AuditExportService — file IO', () => {
    let tmpDir: string;
    let mockPrisma: { auditLog: { findMany: jest.Mock } };
    const fakeNow = () => new Date('2026-05-22T18:00:00Z');

    beforeEach(async () => {
        tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'audit-export-test-'));
        mockPrisma = {
            auditLog: { findMany: jest.fn() },
        };
    });

    afterEach(async () => {
        await fs.rm(tmpDir, { recursive: true, force: true });
    });

    it('writes CSV + CEF for today\'s entries', async () => {
        mockPrisma.auditLog.findMany.mockResolvedValueOnce([sampleEntry]);
        const svc = new AuditExportService({
            prisma: mockPrisma as never,
            outputDir: tmpDir,
            now: fakeNow,
        });
        const result = await svc.exportDay(fakeNow());
        expect(result.written).toBe(true);
        expect(result.rows).toBe(1);

        const csv = await fs.readFile(path.join(tmpDir, 'auditlog-2026-05-22.csv'), 'utf8');
        expect(csv).toContain('id,createdAt,actor_email,action,resource,resourceId,details');
        expect(csv).toContain('admin@example.com,DELETE,Monitor,42');

        const cef = await fs.readFile(path.join(tmpDir, 'auditlog-2026-05-22.cef'), 'utf8');
        expect(cef).toContain('CEF:0|EvidenceAction|UptimeSentinel|1.0|DELETE|');
    });

    it('skips past-day files that already exist (idempotent backfill)', async () => {
        // Write a non-empty file for yesterday first
        const ydn = new Date('2026-05-21T12:00:00Z');
        await fs.writeFile(path.join(tmpDir, 'auditlog-2026-05-21.csv'), 'id,createdAt\n1,2026-05-21T00:00:00Z\n');

        mockPrisma.auditLog.findMany.mockResolvedValueOnce([]);
        const svc = new AuditExportService({
            prisma: mockPrisma as never,
            outputDir: tmpDir,
            now: fakeNow,
        });
        const result = await svc.exportDay(ydn);
        expect(result.written).toBe(false);
        expect(mockPrisma.auditLog.findMany).not.toHaveBeenCalled();
    });

    it('always rewrites today\'s file even when it exists', async () => {
        await fs.writeFile(path.join(tmpDir, 'auditlog-2026-05-22.csv'), 'old content\n');
        mockPrisma.auditLog.findMany.mockResolvedValueOnce([sampleEntry]);
        const svc = new AuditExportService({
            prisma: mockPrisma as never,
            outputDir: tmpDir,
            now: fakeNow,
        });
        const result = await svc.exportDay(fakeNow());
        expect(result.written).toBe(true);
        const csv = await fs.readFile(path.join(tmpDir, 'auditlog-2026-05-22.csv'), 'utf8');
        expect(csv).not.toContain('old content');
        expect(csv).toContain('admin@example.com');
    });

    it('rotates files older than retentionDays', async () => {
        // Write an old file (90+ days ago)
        const oldName = 'auditlog-2026-01-01.csv';
        await fs.writeFile(path.join(tmpDir, oldName), 'old');
        const recentName = 'auditlog-2026-05-15.csv';
        await fs.writeFile(path.join(tmpDir, recentName), 'recent');

        const svc = new AuditExportService({
            prisma: mockPrisma as never,
            outputDir: tmpDir,
            retentionDays: 30,
            now: fakeNow,
        });
        const deleted = await svc.rotateOldExports();
        expect(deleted).toBeGreaterThanOrEqual(1);

        await expect(fs.stat(path.join(tmpDir, oldName))).rejects.toThrow();
        await expect(fs.stat(path.join(tmpDir, recentName))).resolves.toBeDefined();
    });

    it('rotateOldExports returns 0 when the directory is missing (first-run case)', async () => {
        const svc = new AuditExportService({
            prisma: mockPrisma as never,
            outputDir: path.join(tmpDir, 'never-created'),
            retentionDays: 1,
            now: fakeNow,
        });
        expect(await svc.rotateOldExports()).toBe(0);
    });
});
