import { MonitorAuthorization } from '../monitor.authz';
import { AuthorizationError, NotFoundError, AuthContext } from '../types';
import { prisma } from '@/lib/prisma';

jest.mock('@/lib/prisma', () => ({
    prisma: {
        monitor: {
            findUnique: jest.fn(),
            findMany: jest.fn(),
        },
    },
}));

const mockFindUnique = prisma.monitor.findUnique as jest.MockedFunction<typeof prisma.monitor.findUnique>;
const mockFindMany = prisma.monitor.findMany as jest.MockedFunction<typeof prisma.monitor.findMany>;

const ADMIN: AuthContext = { userId: 1, role: 'ADMIN' };
const ADMIN_RO: AuthContext = { userId: 2, role: 'ADMIN_READ_ONLY' };
const EDITOR_5: AuthContext = { userId: 5, role: 'EDITOR' };
const EDITOR_7: AuthContext = { userId: 7, role: 'EDITOR' };
const VIEWER_5: AuthContext = { userId: 5, role: 'VIEWER' };

beforeEach(() => {
    mockFindUnique.mockReset();
    mockFindMany.mockReset();
});

describe('canReadAny / canWriteAny / canDeleteAny', () => {
    it('ADMIN can do anything', () => {
        expect(MonitorAuthorization.canReadAny(ADMIN)).toBe(true);
        expect(MonitorAuthorization.canWriteAny(ADMIN)).toBe(true);
        expect(MonitorAuthorization.canDeleteAny(ADMIN)).toBe(true);
    });

    it('ADMIN_READ_ONLY reads any, writes/deletes none', () => {
        expect(MonitorAuthorization.canReadAny(ADMIN_RO)).toBe(true);
        expect(MonitorAuthorization.canWriteAny(ADMIN_RO)).toBe(false);
        expect(MonitorAuthorization.canDeleteAny(ADMIN_RO)).toBe(false);
    });

    it('EDITOR / VIEWER cannot do any-scope operations', () => {
        expect(MonitorAuthorization.canReadAny(EDITOR_5)).toBe(false);
        expect(MonitorAuthorization.canWriteAny(EDITOR_5)).toBe(false);
        expect(MonitorAuthorization.canReadAny(VIEWER_5)).toBe(false);
        expect(MonitorAuthorization.canWriteAny(VIEWER_5)).toBe(false);
    });
});

describe('assertCanWrite', () => {
    it('allows ADMIN to write any monitor', async () => {
        mockFindUnique.mockResolvedValue({ userId: 42, deletedAt: null } as never);
        await expect(MonitorAuthorization.assertCanWrite(ADMIN, 99)).resolves.toBeUndefined();
    });

    it('denies ADMIN_READ_ONLY', async () => {
        mockFindUnique.mockResolvedValue({ userId: 42, deletedAt: null } as never);
        await expect(MonitorAuthorization.assertCanWrite(ADMIN_RO, 99)).rejects.toBeInstanceOf(AuthorizationError);
    });

    it('allows EDITOR to write their own monitor', async () => {
        mockFindUnique.mockResolvedValue({ userId: 5, deletedAt: null } as never);
        await expect(MonitorAuthorization.assertCanWrite(EDITOR_5, 99)).resolves.toBeUndefined();
    });

    it('denies EDITOR writing another user\'s monitor', async () => {
        mockFindUnique.mockResolvedValue({ userId: 5, deletedAt: null } as never);
        await expect(MonitorAuthorization.assertCanWrite(EDITOR_7, 99)).rejects.toBeInstanceOf(AuthorizationError);
    });

    it('denies VIEWER', async () => {
        mockFindUnique.mockResolvedValue({ userId: 5, deletedAt: null } as never);
        await expect(MonitorAuthorization.assertCanWrite(VIEWER_5, 99)).rejects.toBeInstanceOf(AuthorizationError);
    });

    it('throws NotFoundError for non-existent monitor (even for ADMIN)', async () => {
        mockFindUnique.mockResolvedValue(null);
        await expect(MonitorAuthorization.assertCanWrite(ADMIN, 99)).rejects.toBeInstanceOf(NotFoundError);
    });

    it('throws NotFoundError for soft-deleted monitor (does not leak existence)', async () => {
        mockFindUnique.mockResolvedValue({ userId: 5, deletedAt: new Date() } as never);
        await expect(MonitorAuthorization.assertCanWrite(EDITOR_7, 99)).rejects.toBeInstanceOf(NotFoundError);
    });
});

describe('assertCanDelete', () => {
    it('follows the same policy as write', async () => {
        mockFindUnique.mockResolvedValue({ userId: 5, deletedAt: null } as never);
        await expect(MonitorAuthorization.assertCanDelete(EDITOR_7, 99)).rejects.toBeInstanceOf(AuthorizationError);
        await expect(MonitorAuthorization.assertCanDelete(EDITOR_5, 99)).resolves.toBeUndefined();
    });
});

describe('assertCanWriteMany', () => {
    it('passes when ADMIN owns nothing but can mutate everything', async () => {
        mockFindMany.mockResolvedValue([
            { id: 1, userId: 99, deletedAt: null },
            { id: 2, userId: 88, deletedAt: null },
        ] as never);
        await expect(MonitorAuthorization.assertCanWriteMany(ADMIN, [1, 2])).resolves.toBeUndefined();
    });

    it('passes when EDITOR owns every id', async () => {
        mockFindMany.mockResolvedValue([
            { id: 1, userId: 5, deletedAt: null },
            { id: 2, userId: 5, deletedAt: null },
        ] as never);
        await expect(MonitorAuthorization.assertCanWriteMany(EDITOR_5, [1, 2])).resolves.toBeUndefined();
    });

    it('throws when EDITOR does not own any one id (atomic)', async () => {
        mockFindMany.mockResolvedValue([
            { id: 1, userId: 5, deletedAt: null },
            { id: 2, userId: 7, deletedAt: null },
        ] as never);
        await expect(MonitorAuthorization.assertCanWriteMany(EDITOR_5, [1, 2])).rejects.toBeInstanceOf(AuthorizationError);
    });

    it('throws NotFoundError when any id is missing', async () => {
        mockFindMany.mockResolvedValue([
            { id: 1, userId: 5, deletedAt: null },
        ] as never);
        await expect(MonitorAuthorization.assertCanWriteMany(EDITOR_5, [1, 999])).rejects.toBeInstanceOf(NotFoundError);
    });

    it('rejects VIEWER without hitting the DB', async () => {
        await expect(MonitorAuthorization.assertCanWriteMany(VIEWER_5, [1, 2])).rejects.toBeInstanceOf(AuthorizationError);
        expect(mockFindMany).not.toHaveBeenCalled();
    });

    it('noops on empty input', async () => {
        await expect(MonitorAuthorization.assertCanWriteMany(EDITOR_5, [])).resolves.toBeUndefined();
        expect(mockFindMany).not.toHaveBeenCalled();
    });
});

describe('filterWritable', () => {
    it('returns the IDs EDITOR owns, drops the rest', async () => {
        mockFindMany.mockResolvedValue([
            { id: 1, userId: 5, deletedAt: null },
            { id: 2, userId: 7, deletedAt: null },
            { id: 3, userId: 5, deletedAt: null },
            { id: 4, userId: 5, deletedAt: new Date() }, // soft-deleted, drop
        ] as never);
        const result = await MonitorAuthorization.filterWritable(EDITOR_5, [1, 2, 3, 4]);
        expect(result).toEqual([1, 3]);
    });

    it('returns all non-soft-deleted IDs for ADMIN', async () => {
        mockFindMany.mockResolvedValue([
            { id: 1, userId: 5, deletedAt: null },
            { id: 2, userId: 7, deletedAt: null },
            { id: 3, userId: 9, deletedAt: new Date() },
        ] as never);
        const result = await MonitorAuthorization.filterWritable(ADMIN, [1, 2, 3]);
        expect(result).toEqual([1, 2]);
    });

    it('returns empty for VIEWER without hitting DB', async () => {
        const result = await MonitorAuthorization.filterWritable(VIEWER_5, [1, 2, 3]);
        expect(result).toEqual([]);
        expect(mockFindMany).not.toHaveBeenCalled();
    });
});
