const auditService = require('../../../../modules/oidc/auditService');
const { AuthAuditLog, User } = require('../../../../models');
const { sequelize } = require('../../../../models');
const bcrypt = require('bcrypt');

describe('OIDC Audit Service', () => {
    let mockReq;
    let testUser;

    beforeAll(async () => {
        await sequelize.sync({ force: true });
    });

    afterAll(async () => {
        await sequelize.close();
    });

    beforeEach(async () => {
        await AuthAuditLog.destroy({ where: {}, truncate: true });
        await User.destroy({ where: {}, truncate: true });

        testUser = await User.create({
            email: 'test@example.com',
            password_digest: await bcrypt.hash('password123', 10),
        });

        mockReq = {
            ip: '192.168.1.1',
            connection: { remoteAddress: '192.168.1.1' },
            get: jest.fn((header) => {
                if (header === 'user-agent') return 'Mozilla/5.0 Test Browser';
                return null;
            }),
        };
    });

    describe('EVENT_TYPES and AUTH_METHODS constants', () => {
        it('should export EVENT_TYPES', () => {
            expect(auditService.EVENT_TYPES).toBeDefined();
            expect(auditService.EVENT_TYPES.LOGIN_SUCCESS).toBe(
                'login_success'
            );
            expect(auditService.EVENT_TYPES.LOGIN_FAILED).toBe('login_failed');
            expect(auditService.EVENT_TYPES.OIDC_LINKED).toBe('oidc_linked');
        });

        it('should export AUTH_METHODS', () => {
            expect(auditService.AUTH_METHODS).toBeDefined();
            expect(auditService.AUTH_METHODS.EMAIL_PASSWORD).toBe(
                'email_password'
            );
            expect(auditService.AUTH_METHODS.OIDC).toBe('oidc');
            expect(auditService.AUTH_METHODS.API_TOKEN).toBe('api_token');
        });
    });

    describe('logEvent', () => {
        it('should create audit log entry', async () => {
            await auditService.logEvent({
                userId: testUser.id,
                eventType: 'login_success',
                authMethod: 'email_password',
                ipAddress: '192.168.1.1',
                userAgent: 'Test Browser',
            });

            const logs = await AuthAuditLog.findAll();
            expect(logs).toHaveLength(1);
            expect(logs[0].user_id).toBe(testUser.id);
            expect(logs[0].event_type).toBe('login_success');
            expect(logs[0].auth_method).toBe('email_password');
        });

        it('should store metadata as JSON string', async () => {
            await auditService.logEvent({
                userId: testUser.id,
                eventType: 'login_failed',
                authMethod: 'email_password',
                metadata: {
                    email: 'test@example.com',
                    reason: 'invalid_password',
                },
            });

            const log = await AuthAuditLog.findOne();
            expect(log.metadata).toBe(
                '{"email":"test@example.com","reason":"invalid_password"}'
            );
        });

        it('should handle null userId for failed login attempts', async () => {
            await auditService.logEvent({
                userId: null,
                eventType: 'login_failed',
                authMethod: 'email_password',
            });

            const log = await AuthAuditLog.findOne();
            expect(log.user_id).toBeNull();
        });

        it('should not throw on logging errors', async () => {
            await expect(
                auditService.logEvent({
                    userId: 999999,
                    eventType: null,
                    authMethod: null,
                })
            ).resolves.not.toThrow();
        });
    });

    describe('logLoginSuccess', () => {
        it('should log successful email/password login', async () => {
            await auditService.logLoginSuccess(
                testUser.id,
                auditService.AUTH_METHODS.EMAIL_PASSWORD,
                mockReq
            );

            const log = await AuthAuditLog.findOne();
            expect(log.user_id).toBe(testUser.id);
            expect(log.event_type).toBe('login_success');
            expect(log.auth_method).toBe('email_password');
            expect(log.ip_address).toBe('192.168.1.1');
            expect(log.user_agent).toBe('Mozilla/5.0 Test Browser');
            expect(log.provider_slug).toBeNull();
        });

        it('should log successful OIDC login with provider', async () => {
            await auditService.logLoginSuccess(
                testUser.id,
                auditService.AUTH_METHODS.OIDC,
                mockReq,
                'google'
            );

            const log = await AuthAuditLog.findOne();
            expect(log.user_id).toBe(testUser.id);
            expect(log.event_type).toBe('login_success');
            expect(log.auth_method).toBe('oidc');
            expect(log.provider_slug).toBe('google');
        });
    });

    describe('logLoginFailed', () => {
        it('should log failed login attempt with email', async () => {
            await auditService.logLoginFailed(
                'test@example.com',
                auditService.AUTH_METHODS.EMAIL_PASSWORD,
                mockReq,
                null,
                'invalid_password'
            );

            const log = await AuthAuditLog.findOne();
            expect(log.user_id).toBeNull();
            expect(log.event_type).toBe('login_failed');
            expect(log.auth_method).toBe('email_password');

            const metadata = JSON.parse(log.metadata);
            expect(metadata.email).toBe('test@example.com');
            expect(metadata.reason).toBe('invalid_password');
        });

        it('should log failed OIDC attempt', async () => {
            await auditService.logLoginFailed(
                'user@example.com',
                auditService.AUTH_METHODS.OIDC,
                mockReq,
                'google',
                'auto_provision_disabled'
            );

            const log = await AuthAuditLog.findOne();
            expect(log.provider_slug).toBe('google');
            expect(log.auth_method).toBe('oidc');
        });
    });

    describe('logLogout', () => {
        it('should log logout event', async () => {
            await auditService.logLogout(testUser.id, mockReq);

            const log = await AuthAuditLog.findOne();
            expect(log.user_id).toBe(testUser.id);
            expect(log.event_type).toBe('logout');
            expect(log.ip_address).toBe('192.168.1.1');
        });
    });

    describe('logOidcLinked', () => {
        it('should log OIDC account linking', async () => {
            await auditService.logOidcLinked(testUser.id, 'google', mockReq);

            const log = await AuthAuditLog.findOne();
            expect(log.user_id).toBe(testUser.id);
            expect(log.event_type).toBe('oidc_linked');
            expect(log.auth_method).toBe('oidc');
            expect(log.provider_slug).toBe('google');
        });
    });

    describe('logOidcUnlinked', () => {
        it('should log OIDC account unlinking', async () => {
            await auditService.logOidcUnlinked(testUser.id, 'okta', mockReq);

            const log = await AuthAuditLog.findOne();
            expect(log.user_id).toBe(testUser.id);
            expect(log.event_type).toBe('oidc_unlinked');
            expect(log.provider_slug).toBe('okta');
        });
    });

    describe('logOidcProvision', () => {
        it('should log new user provisioning', async () => {
            await auditService.logOidcProvision(
                testUser.id,
                'google',
                mockReq,
                true
            );

            const log = await AuthAuditLog.findOne();
            expect(log.user_id).toBe(testUser.id);
            expect(log.event_type).toBe('oidc_provision');
            expect(log.provider_slug).toBe('google');

            const metadata = JSON.parse(log.metadata);
            expect(metadata.isNewUser).toBe(true);
        });

        it('should log existing user provisioning', async () => {
            await auditService.logOidcProvision(
                testUser.id,
                'okta',
                mockReq,
                false
            );

            const log = await AuthAuditLog.findOne();
            const metadata = JSON.parse(log.metadata);
            expect(metadata.isNewUser).toBe(false);
        });
    });

    describe('getRecentEvents', () => {
        beforeEach(async () => {
            for (let i = 1; i <= 60; i++) {
                await auditService.logEvent({
                    userId: testUser.id,
                    eventType: 'login_success',
                    authMethod: 'email_password',
                });
            }
        });

        it('should return recent events for user', async () => {
            const events = await auditService.getRecentEvents(testUser.id);
            expect(events).toHaveLength(50);
        });

        it('should return events in descending order', async () => {
            const events = await auditService.getRecentEvents(testUser.id, 5);

            expect(events).toHaveLength(5);
            for (let i = 0; i < events.length - 1; i++) {
                expect(
                    new Date(events[i].created_at) >=
                        new Date(events[i + 1].created_at)
                ).toBe(true);
            }
        });

        it('should respect custom limit', async () => {
            const events = await auditService.getRecentEvents(testUser.id, 10);
            expect(events).toHaveLength(10);
        });

        it('should return empty array for user with no events', async () => {
            const events = await auditService.getRecentEvents(999);
            expect(events).toHaveLength(0);
        });
    });

    describe('cleanupOldLogs', () => {
        beforeEach(async () => {
            await auditService.logEvent({
                userId: testUser.id,
                eventType: 'login_success',
                authMethod: 'email_password',
            });

            const oldLog = await AuthAuditLog.create({
                user_id: testUser.id,
                event_type: 'login_success',
                auth_method: 'email_password',
                created_at: new Date(Date.now() - 100 * 24 * 60 * 60 * 1000),
            });
        });

        it('should delete logs older than specified days', async () => {
            const deletedCount = await auditService.cleanupOldLogs(90);

            expect(deletedCount).toBe(1);

            const remaining = await AuthAuditLog.count();
            expect(remaining).toBe(1);
        });

        it('should not delete recent logs', async () => {
            const deletedCount = await auditService.cleanupOldLogs(200);

            expect(deletedCount).toBe(0);

            const remaining = await AuthAuditLog.count();
            expect(remaining).toBe(2);
        });

        it('should respect custom retention period', async () => {
            const deletedCount = await auditService.cleanupOldLogs(30);

            expect(deletedCount).toBe(1);
        });
    });

    describe('IP address handling', () => {
        it('should use req.ip if available', async () => {
            mockReq.ip = '10.0.0.1';
            await auditService.logLoginSuccess(
                testUser.id,
                'email_password',
                mockReq
            );

            const log = await AuthAuditLog.findOne();
            expect(log.ip_address).toBe('10.0.0.1');
        });

        it('should fallback to req.connection.remoteAddress', async () => {
            mockReq.ip = null;
            mockReq.connection.remoteAddress = '172.16.0.1';

            await auditService.logLoginSuccess(
                testUser.id,
                'email_password',
                mockReq
            );

            const log = await AuthAuditLog.findOne();
            expect(log.ip_address).toBe('172.16.0.1');
        });
    });
});
