const {
    getAccess,
    getSharedUidsForUser,
    ownershipOrPermissionWhere,
    ACCESS,
} = require('../../../services/permissionsService');
const {
    User,
    Project,
    Task,
    Note,
    Permission,
    sequelize,
} = require('../../../models');
const bcrypt = require('bcrypt');

describe('permissionsService', () => {
    let owner, otherUser, adminUser;

    beforeEach(async () => {
        await sequelize.query('DELETE FROM permissions');
        await sequelize.query('DELETE FROM roles');
        const hash = await bcrypt.hash('pass', 10);
        // First user created gets admin role automatically via afterCreate hook
        adminUser = await User.create({
            email: 'admin@test.com',
            password_digest: hash,
        });
        owner = await User.create({
            email: 'owner@test.com',
            password_digest: hash,
        });
        otherUser = await User.create({
            email: 'other@test.com',
            password_digest: hash,
        });
    });

    describe('getAccess', () => {
        // --- Projects ---

        it('should return rw for project owner', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            const access = await getAccess(owner.id, 'project', project.uid);
            expect(access).toBe('rw');
        });

        it('should return none for non-owner without permission', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            const access = await getAccess(
                otherUser.id,
                'project',
                project.uid
            );
            expect(access).toBe('none');
        });

        it('should return admin for admin user', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            const access = await getAccess(
                adminUser.uid,
                'project',
                project.uid
            );
            expect(access).toBe('admin');
        });

        it('should return none for non-existent project', async () => {
            const access = await getAccess(
                owner.id,
                'project',
                'nonexistent-uid'
            );
            expect(access).toBe('none');
        });

        it('should return shared permission level for project', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            await Permission.create({
                user_id: otherUser.id,
                resource_type: 'project',
                resource_uid: project.uid,
                access_level: 'ro',
                propagation: 'direct',
                granted_by_user_id: owner.id,
            });
            const access = await getAccess(
                otherUser.id,
                'project',
                project.uid
            );
            expect(access).toBe('ro');
        });

        // --- Tasks ---

        it('should return rw for task owner', async () => {
            const task = await Task.create({ name: 'T1', user_id: owner.id });
            const access = await getAccess(owner.id, 'task', task.uid);
            expect(access).toBe('rw');
        });

        it('should return none for non-owner task without permission', async () => {
            const task = await Task.create({ name: 'T1', user_id: owner.id });
            const access = await getAccess(otherUser.id, 'task', task.uid);
            expect(access).toBe('none');
        });

        it('should return none for non-existent task', async () => {
            const access = await getAccess(owner.id, 'task', 'no-such-task');
            expect(access).toBe('none');
        });

        it('should inherit task access from parent project permission', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            const task = await Task.create({
                name: 'T1',
                user_id: owner.id,
                project_id: project.id,
            });
            await Permission.create({
                user_id: otherUser.id,
                resource_type: 'project',
                resource_uid: project.uid,
                access_level: 'rw',
                propagation: 'direct',
                granted_by_user_id: owner.id,
            });
            const access = await getAccess(otherUser.id, 'task', task.uid);
            expect(access).toBe('rw');
        });

        it('should return shared permission for directly shared task', async () => {
            const task = await Task.create({ name: 'T1', user_id: owner.id });
            await Permission.create({
                user_id: otherUser.id,
                resource_type: 'task',
                resource_uid: task.uid,
                access_level: 'ro',
                propagation: 'direct',
                granted_by_user_id: owner.id,
            });
            const access = await getAccess(otherUser.id, 'task', task.uid);
            expect(access).toBe('ro');
        });

        // --- Notes ---

        it('should return rw for note owner', async () => {
            const note = await Note.create({ title: 'N1', user_id: owner.id });
            const access = await getAccess(owner.id, 'note', note.uid);
            expect(access).toBe('rw');
        });

        it('should return none for non-owner note without permission', async () => {
            const note = await Note.create({ title: 'N1', user_id: owner.id });
            const access = await getAccess(otherUser.id, 'note', note.uid);
            expect(access).toBe('none');
        });

        it('should inherit note access from parent project permission', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            const note = await Note.create({
                title: 'N1',
                user_id: owner.id,
                project_id: project.id,
            });
            await Permission.create({
                user_id: otherUser.id,
                resource_type: 'project',
                resource_uid: project.uid,
                access_level: 'ro',
                propagation: 'direct',
                granted_by_user_id: owner.id,
            });
            const access = await getAccess(otherUser.id, 'note', note.uid);
            expect(access).toBe('ro');
        });
    });

    describe('getSharedUidsForUser', () => {
        it('should return empty array when no permissions exist', async () => {
            const uids = await getSharedUidsForUser('project', otherUser.id);
            expect(uids).toEqual([]);
        });

        it('should return shared resource uids', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            await Permission.create({
                user_id: otherUser.id,
                resource_type: 'project',
                resource_uid: project.uid,
                access_level: 'ro',
                propagation: 'direct',
                granted_by_user_id: owner.id,
            });
            const uids = await getSharedUidsForUser('project', otherUser.id);
            expect(uids).toContain(project.uid);
        });

        it('should deduplicate uids', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            // Create two permissions for the same resource (different propagation)
            await Permission.create({
                user_id: otherUser.id,
                resource_type: 'project',
                resource_uid: project.uid,
                access_level: 'ro',
                propagation: 'direct',
                granted_by_user_id: owner.id,
            });
            const uids = await getSharedUidsForUser('project', otherUser.id);
            const uniqueUids = [...new Set(uids)];
            expect(uids.length).toBe(uniqueUids.length);
        });
    });

    describe('ownershipOrPermissionWhere', () => {
        it('should include user_id condition for owned resources', async () => {
            const where = await ownershipOrPermissionWhere('project', owner.id);
            expect(where).toBeDefined();
            // Should contain an Op.or with user_id condition
            const orKey = Object.getOwnPropertySymbols(where)[0];
            expect(orKey).toBeDefined();
            const conditions = where[orKey];
            expect(conditions.some((c) => c.user_id === owner.id)).toBe(true);
        });

        it('should include shared resource uids when permissions exist', async () => {
            const project = await Project.create({
                name: 'Shared',
                user_id: owner.id,
            });
            await Permission.create({
                user_id: otherUser.id,
                resource_type: 'project',
                resource_uid: project.uid,
                access_level: 'rw',
                propagation: 'direct',
                granted_by_user_id: owner.id,
            });

            const where = await ownershipOrPermissionWhere(
                'project',
                otherUser.id
            );
            const orKey = Object.getOwnPropertySymbols(where)[0];
            const conditions = where[orKey];
            // Should have a uid IN condition with the shared project uid
            const uidCondition = conditions.find((c) => c.uid);
            expect(uidCondition).toBeDefined();
        });

        it('should use cache when provided', async () => {
            const cache = new Map();
            const where1 = await ownershipOrPermissionWhere(
                'project',
                owner.id,
                cache
            );
            const where2 = await ownershipOrPermissionWhere(
                'project',
                owner.id,
                cache
            );
            expect(where1).toBe(where2); // same reference from cache
        });

        it('should include tasks from shared projects for task resource type', async () => {
            const project = await Project.create({
                name: 'P1',
                user_id: owner.id,
            });
            await Task.create({
                name: 'T1',
                user_id: owner.id,
                project_id: project.id,
            });
            await Permission.create({
                user_id: otherUser.id,
                resource_type: 'project',
                resource_uid: project.uid,
                access_level: 'rw',
                propagation: 'direct',
                granted_by_user_id: owner.id,
            });

            const where = await ownershipOrPermissionWhere(
                'task',
                otherUser.id
            );
            const orKey = Object.getOwnPropertySymbols(where)[0];
            const conditions = where[orKey];
            // Should have a project_id IN condition
            const projectCondition = conditions.find((c) => c.project_id);
            expect(projectCondition).toBeDefined();
        });
    });

    describe('ACCESS constants', () => {
        it('should export expected access levels', () => {
            expect(ACCESS.NONE).toBe('none');
            expect(ACCESS.RO).toBe('ro');
            expect(ACCESS.RW).toBe('rw');
            expect(ACCESS.ADMIN).toBe('admin');
        });
    });
});
