/**
 * SmartAuth comprehensive tests — all phases.
 *
 * Phase 0A: OneAuth schema backward compatibility
 * Phase 0B: Lockout + rate limiting
 * Phase 1A: Google OAuth types + provider registry
 * Phase 1B: Account linking types
 * Phase 2A: TOTP MFA types + challenge store + base32
 * Phase 2B: Microsoft + Apple OAuth types
 * Phase 2C: MFA admin policies
 * Phase 3A: WebAuthn passkey types
 * Phase 3B: Device trust types
 * Phase 4A: Login events + risk scoring
 */

import { describe, it, expect, vi } from 'vitest';
import {
  LoginSchema,
  type UserDoc,
  type ProductMembership,
  type AuthProviderDoc,
} from './types.js';

// Stub config so OAuth module imports don't blow up on missing env vars
vi.mock('../../lib/config.js', () => ({
  config: {
    PORT: 4003,
    HOST: '0.0.0.0',
    NODE_ENV: 'test',
    SERVICE_NAME: 'platform-service',
    COSMOS_ENDPOINT: 'https://fake.documents.azure.com:443/',
    COSMOS_KEY: 'dGVzdA==',
    COSMOS_DATABASE: 'test',
    JWT_SECRET: 'test-secret-at-least-32-chars-long!!',
    MICROSOFT_TENANT_ID: 'common',
    WEBAUTHN_RP_ID: 'localhost',
    WEBAUTHN_RP_NAME: 'ByteLyst Test',
    USAGE_WARN_THRESHOLD: 0.8,
    BACKEND_URL: 'http://localhost:8000',
  },
}));

// ── Phase 0A: Schema backward compatibility ─────────────────

describe('OneAuth Schema Extension', () => {
  const baseUser: UserDoc = {
    id: 'usr_1',
    productId: 'lysnrai',
    email: 'test@example.com',
    passwordHash: 'hash',
    plan: 'free',
    role: 'user',
    displayName: 'Test User',
    status: 'active',
    emailVerified: false,
    lastLoginAt: null,
    createdAt: '2026-01-01T00:00:00.000Z',
    updatedAt: '2026-01-01T00:00:00.000Z',
  };

  it('UserDoc is backward compatible — new fields are optional', () => {
    const user: UserDoc = { ...baseUser };
    expect(user.primaryProductId).toBeUndefined();
    expect(user.memberships).toBeUndefined();
    expect(user.mfaEnabled).toBeUndefined();
    expect(user.mfaMethods).toBeUndefined();
    expect(user.lockedUntil).toBeUndefined();
    expect(user.failedLoginAttempts).toBeUndefined();
  });

  it('UserDoc accepts OneAuth fields', () => {
    const user: UserDoc = {
      ...baseUser,
      primaryProductId: 'lysnrai',
      memberships: [
        {
          productId: 'lysnrai',
          plan: 'free',
          role: 'user',
          firstAccessAt: '2026-01-01T00:00:00.000Z',
        },
      ],
      mfaEnabled: false,
      mfaMethods: [],
      lockedUntil: null,
      failedLoginAttempts: 0,
    };
    expect(user.primaryProductId).toBe('lysnrai');
    expect(user.memberships).toHaveLength(1);
    expect(user.mfaEnabled).toBe(false);
    expect(user.failedLoginAttempts).toBe(0);
  });

  it('ProductMembership includes optional subscription/license IDs', () => {
    const membership: ProductMembership = {
      productId: 'mindlyst',
      plan: 'pro',
      role: 'admin',
      firstAccessAt: '2026-02-01T00:00:00.000Z',
      subscriptionId: 'sub_123',
      licenseId: 'lic_456',
    };
    expect(membership.subscriptionId).toBe('sub_123');
    expect(membership.licenseId).toBe('lic_456');
  });

  it('AuthProviderDoc follows expected schema', () => {
    const doc: AuthProviderDoc = {
      id: 'usr_1:google',
      productId: 'smartauth',
      userId: 'usr_1',
      provider: 'google',
      providerUserId: 'google_123',
      email: 'test@gmail.com',
      linkedAt: '2026-03-01T00:00:00.000Z',
      lastUsedAt: '2026-03-01T00:00:00.000Z',
    };
    expect(doc.id).toBe('usr_1:google');
    expect(doc.productId).toBe('smartauth');
    expect(doc.provider).toBe('google');
  });
});

// ── Phase 0A: Cross-product lookup + backfill ───────────────

describe('OneAuth repository helpers', () => {
  it('getByEmailCrossProduct is exported', async () => {
    const repo = await import('./repository.js');
    expect(typeof repo.getByEmailCrossProduct).toBe('function');
  });

  it('backfillMemberships is exported', async () => {
    const repo = await import('./repository.js');
    expect(typeof repo.backfillMemberships).toBe('function');
  });
});

// ── Phase 0B: Lockout + Rate Limiting ───────────────────────

describe('Lockout error codes', () => {
  it('LoginSchema requires productId', () => {
    const result = LoginSchema.safeParse({ email: 'test@example.com', password: 'secret' });
    expect(result.success).toBe(false);
  });

  it('LoginSchema accepts valid login with productId', () => {
    const result = LoginSchema.safeParse({
      email: 'test@example.com',
      password: 'secret',
      productId: 'lysnrai',
    });
    expect(result.success).toBe(true);
  });
});

describe('Lockout repository helpers', () => {
  it('incrementFailedLogin is exported', async () => {
    const repo = await import('./repository.js');
    expect(typeof repo.incrementFailedLogin).toBe('function');
  });

  it('resetFailedLogin is exported', async () => {
    const repo = await import('./repository.js');
    expect(typeof repo.resetFailedLogin).toBe('function');
  });
});

// ── Phase 1A: Google OAuth types ────────────────────────────

describe('OAuth schemas', () => {
  it('OAuthGoogleSchema validates idToken + productId', async () => {
    const { OAuthGoogleSchema } = await import('./oauth/types.js');
    expect(OAuthGoogleSchema.safeParse({ idToken: 'tok', productId: 'lysnrai' }).success).toBe(
      true
    );
    expect(OAuthGoogleSchema.safeParse({ idToken: '' }).success).toBe(false);
    expect(OAuthGoogleSchema.safeParse({}).success).toBe(false);
  });

  it('OAuthMicrosoftSchema validates idToken + productId', async () => {
    const { OAuthMicrosoftSchema } = await import('./oauth/types.js');
    expect(OAuthMicrosoftSchema.safeParse({ idToken: 'tok', productId: 'mindlyst' }).success).toBe(
      true
    );
    expect(OAuthMicrosoftSchema.safeParse({}).success).toBe(false);
  });

  it('OAuthAppleSchema accepts optional displayName', async () => {
    const { OAuthAppleSchema } = await import('./oauth/types.js');
    expect(OAuthAppleSchema.safeParse({ idToken: 'tok', productId: 'x' }).success).toBe(true);
    expect(
      OAuthAppleSchema.safeParse({ idToken: 'tok', productId: 'x', displayName: 'Jane' }).success
    ).toBe(true);
  });
});

describe('OAuth provider registry', () => {
  it('exports verifyIdToken and registerVerifier', async () => {
    const providers = await import('./oauth/providers.js');
    expect(typeof providers.verifyIdToken).toBe('function');
    expect(typeof providers.registerVerifier).toBe('function');
  });

  it('rejects unknown provider', async () => {
    const providers = await import('./oauth/providers.js');
    await expect(providers.verifyIdToken('facebook' as 'google', 'tok')).rejects.toThrow(
      'not configured'
    );
  });
});

// ── Phase 2A: TOTP MFA types + challenge store ─────────────

describe('MFA schemas', () => {
  it('MfaVerifySetupSchema requires 6-digit code', async () => {
    const { MfaVerifySetupSchema } = await import('./mfa/types.js');
    expect(MfaVerifySetupSchema.safeParse({ code: '123456' }).success).toBe(true);
    expect(MfaVerifySetupSchema.safeParse({ code: '12345' }).success).toBe(false);
    expect(MfaVerifySetupSchema.safeParse({ code: '1234567' }).success).toBe(false);
  });

  it('MfaVerifySchema requires challengeToken + code', async () => {
    const { MfaVerifySchema } = await import('./mfa/types.js');
    expect(MfaVerifySchema.safeParse({ challengeToken: 'mfa_xxx', code: '123456' }).success).toBe(
      true
    );
    expect(MfaVerifySchema.safeParse({ challengeToken: '', code: '123456' }).success).toBe(false);
  });

  it('MfaPolicySchema has defaults', async () => {
    const { MfaPolicySchema } = await import('./mfa/types.js');
    const result = MfaPolicySchema.safeParse({ required: true });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.methods).toEqual(['totp']);
      expect(result.data.gracePeriodDays).toBe(7);
    }
  });
});

describe('MFA challenge store', () => {
  it('creates and consumes challenges', async () => {
    const { createChallenge, consumeChallenge, _clearChallenges } =
      await import('./mfa/challenge-store.js');
    _clearChallenges();

    const token = createChallenge({
      userId: 'usr_1',
      productId: 'lysnrai',
      email: 'a@b.com',
      role: 'user',
      plan: 'free',
    });
    expect(token).toMatch(/^mfa_/);

    const challenge = consumeChallenge(token);
    expect(challenge).not.toBeNull();
    expect(challenge?.userId).toBe('usr_1');

    // Second consumption should return null (already consumed)
    const again = consumeChallenge(token);
    expect(again).toBeNull();
  });

  it('returns null for unknown challenge token', async () => {
    const { consumeChallenge } = await import('./mfa/challenge-store.js');
    expect(consumeChallenge('mfa_nonexistent')).toBeNull();
  });
});

describe('MFA TOTP base32 + verify', () => {
  it('base32Encode produces valid output', async () => {
    const { _base32Encode } = await import('./mfa/routes.js');
    const encoded = _base32Encode(Buffer.from('hello'));
    expect(encoded).toBe('NBSWY3DP');
  });
});

describe('MFA repository exports', () => {
  it('exports encryption helpers', async () => {
    const repo = await import('./mfa/repository.js');
    expect(typeof repo.encryptSecret).toBe('function');
    expect(typeof repo.decryptSecret).toBe('function');
    expect(typeof repo.generateRecoveryCodes).toBe('function');
    expect(typeof repo.hashRecoveryCode).toBe('function');
  });

  it('generates 8 recovery codes by default', async () => {
    const repo = await import('./mfa/repository.js');
    const codes = repo.generateRecoveryCodes();
    expect(codes).toHaveLength(8);
    // Each code is 8 hex chars
    for (const code of codes) {
      expect(code).toMatch(/^[0-9a-f]{8}$/);
    }
  });

  it('hashRecoveryCode is deterministic', async () => {
    const repo = await import('./mfa/repository.js');
    const h1 = repo.hashRecoveryCode('abc123');
    const h2 = repo.hashRecoveryCode('abc123');
    expect(h1).toBe(h2);
    expect(h1).toHaveLength(64); // SHA-256 hex
  });
});

// ── Phase 3A: WebAuthn Passkeys ─────────────────────────────

describe('Passkey types', () => {
  it('PasskeyDoc satisfies expected structure', () => {
    const doc: import('./passkeys/types.js').PasskeyDoc = {
      id: 'pk_abc',
      userId: 'usr_1',
      productId: 'smartauth',
      credentialId: 'abc',
      publicKey: 'key',
      counter: 0,
      deviceType: 'multiDevice',
      backedUp: false,
      createdAt: '2026-01-01T00:00:00.000Z',
      lastUsedAt: '2026-01-01T00:00:00.000Z',
    };
    expect(doc.counter).toBe(0);
    expect(doc.deviceType).toBe('multiDevice');
  });

  it('passkey repository exports CRUD functions', async () => {
    const repo = await import('./passkeys/repository.js');
    expect(typeof repo.create).toBe('function');
    expect(typeof repo.getByCredentialId).toBe('function');
    expect(typeof repo.listByUser).toBe('function');
    expect(typeof repo.updateCounter).toBe('function');
    expect(typeof repo.remove).toBe('function');
  });
});

// ── Phase 3B: Device Trust ──────────────────────────────────

describe('Device trust types', () => {
  it('TRUST_DURATIONS_DAYS has correct values', async () => {
    const { TRUST_DURATIONS_DAYS } = await import('./devices/types.js');
    expect(TRUST_DURATIONS_DAYS.trusted).toBe(90);
    expect(TRUST_DURATIONS_DAYS.remembered).toBe(30);
    expect(TRUST_DURATIONS_DAYS.unknown).toBe(0);
  });

  it('TrustDeviceSchema validates input', async () => {
    const { TrustDeviceSchema } = await import('./devices/types.js');
    expect(TrustDeviceSchema.safeParse({ fingerprint: 'abc', trustLevel: 'trusted' }).success).toBe(
      true
    );
    expect(TrustDeviceSchema.safeParse({ fingerprint: 'abc', trustLevel: 'invalid' }).success).toBe(
      false
    );
    expect(TrustDeviceSchema.safeParse({ fingerprint: '', trustLevel: 'trusted' }).success).toBe(
      false
    );
  });

  it('device repository exports functions', async () => {
    const repo = await import('./devices/repository.js');
    expect(typeof repo.getByFingerprint).toBe('function');
    expect(typeof repo.upsert).toBe('function');
    expect(typeof repo.listByUser).toBe('function');
    expect(typeof repo.revokeTrust).toBe('function');
    expect(typeof repo.revokeAllTrust).toBe('function');
    expect(typeof repo.isDeviceTrusted).toBe('function');
  });

  it('isDeviceTrusted returns false for null', async () => {
    const repo = await import('./devices/repository.js');
    expect(repo.isDeviceTrusted(null)).toBe(false);
  });
});

// ── Phase 4A: Login Events + Risk Scoring ───────────────────

describe('Login risk scorer', () => {
  it('scores low for trusted device, known IP', async () => {
    const { scoreLoginRisk } = await import('./login-events/risk-scorer.js');
    const result = scoreLoginRisk({
      ip: '1.2.3.4',
      isNewIp: false,
      isNewDevice: false,
      isDeviceTrusted: true,
      recentFailures: 0,
      method: 'password',
      hourOfDay: 10,
    });
    expect(result.score).toBe(0);
    expect(result.level).toBe('low');
    expect(result.flags).toHaveLength(0);
  });

  it('scores medium for new device', async () => {
    const { scoreLoginRisk } = await import('./login-events/risk-scorer.js');
    const result = scoreLoginRisk({
      ip: '1.2.3.4',
      isNewIp: false,
      isNewDevice: true,
      isDeviceTrusted: false,
      recentFailures: 0,
      method: 'oauth_google',
      hourOfDay: 10,
    });
    expect(result.score).toBeGreaterThanOrEqual(25);
    expect(result.level).toBe('medium');
    expect(result.flags).toContain('new_device');
    expect(result.flags).toContain('untrusted_device');
  });

  it('scores high for new IP + new device + failures', async () => {
    const { scoreLoginRisk } = await import('./login-events/risk-scorer.js');
    const result = scoreLoginRisk({
      ip: '9.9.9.9',
      isNewIp: true,
      isNewDevice: true,
      isDeviceTrusted: false,
      recentFailures: 5,
      method: 'password',
      hourOfDay: 3,
    });
    expect(result.score).toBeGreaterThanOrEqual(50);
    expect(['high', 'critical']).toContain(result.level);
    expect(result.flags).toContain('new_ip');
    expect(result.flags).toContain('new_device');
    expect(result.flags).toContain('unusual_hour');
    expect(result.flags).toContain('password_new_device');
  });

  it('max score is capped and level is critical', async () => {
    const { scoreLoginRisk } = await import('./login-events/risk-scorer.js');
    const result = scoreLoginRisk({
      ip: '9.9.9.9',
      isNewIp: true,
      isNewDevice: true,
      isDeviceTrusted: false,
      recentFailures: 50,
      method: 'password',
      hourOfDay: 3,
    });
    // 15(new_ip) + 20(new_device) + 10(untrusted) + 30(failures cap) + 10(unusual_hour) + 10(password_new_device) = 95
    expect(result.score).toBe(95);
    expect(result.level).toBe('critical');
  });
});

describe('Login event types', () => {
  it('LoginEventQuerySchema has sensible defaults', async () => {
    const { LoginEventQuerySchema } = await import('./login-events/types.js');
    const result = LoginEventQuerySchema.safeParse({});
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.limit).toBe(50);
    }
  });

  it('login event repository exports CRUD functions', async () => {
    const repo = await import('./login-events/repository.js');
    expect(typeof repo.record).toBe('function');
    expect(typeof repo.listByUser).toBe('function');
    expect(typeof repo.countRecentFailures).toBe('function');
  });
});

// ── Login flow integration tests ─────────────────────────────

describe('Login flow — MFA challenge integration', () => {
  it('createChallenge from mfa/challenge-store returns mfa_ prefixed token', async () => {
    const { createChallenge, consumeChallenge, _clearChallenges } =
      await import('./mfa/challenge-store.js');
    _clearChallenges();
    const token = createChallenge({
      userId: 'usr_mfa',
      productId: 'lysnrai',
      email: 'mfa@test.com',
      role: 'user',
      plan: 'pro',
    });
    expect(token).toMatch(/^mfa_/);
    const data = consumeChallenge(token);
    expect(data?.plan).toBe('pro');
  });
});

describe('Risk scorer — edge cases', () => {
  it('returns low for OAuth on known device', async () => {
    const { scoreLoginRisk } = await import('./login-events/risk-scorer.js');
    const result = scoreLoginRisk({
      ip: '10.0.0.1',
      isNewIp: false,
      isNewDevice: false,
      isDeviceTrusted: true,
      recentFailures: 0,
      method: 'oauth_google',
      hourOfDay: 14,
    });
    expect(result.score).toBe(0);
    expect(result.level).toBe('low');
  });

  it('password on new device at 3am scores higher than OAuth same conditions', async () => {
    const { scoreLoginRisk } = await import('./login-events/risk-scorer.js');
    const passwordResult = scoreLoginRisk({
      ip: '5.5.5.5',
      isNewIp: true,
      isNewDevice: true,
      isDeviceTrusted: false,
      recentFailures: 0,
      method: 'password',
      hourOfDay: 3,
    });
    const oauthResult = scoreLoginRisk({
      ip: '5.5.5.5',
      isNewIp: true,
      isNewDevice: true,
      isDeviceTrusted: false,
      recentFailures: 0,
      method: 'oauth_google',
      hourOfDay: 3,
    });
    expect(passwordResult.score).toBeGreaterThan(oauthResult.score);
    expect(passwordResult.flags).toContain('password_new_device');
    expect(oauthResult.flags).not.toContain('password_new_device');
  });
});

describe('Login events wiring exports', () => {
  it('login-events/repository exports record + listByUser + countRecentFailures', async () => {
    const repo = await import('./login-events/repository.js');
    expect(typeof repo.record).toBe('function');
    expect(typeof repo.listByUser).toBe('function');
    expect(typeof repo.countRecentFailures).toBe('function');
  });

  it('devices/repository exports isDeviceTrusted as pure function', async () => {
    const repo = await import('./devices/repository.js');
    // isDeviceTrusted is a sync function that works without DB
    expect(repo.isDeviceTrusted(null)).toBe(false);
    expect(
      repo.isDeviceTrusted({
        id: 'dev_x',
        userId: 'u',
        productId: 'smartauth',
        fingerprint: 'x',
        trustLevel: 'trusted',
        deviceInfo: {},
        trustExpiresAt: new Date(Date.now() + 86400000).toISOString(),
        createdAt: '2026-01-01T00:00:00Z',
        lastSeenAt: '2026-01-01T00:00:00Z',
      })
    ).toBe(true);
    expect(
      repo.isDeviceTrusted({
        id: 'dev_x',
        userId: 'u',
        productId: 'smartauth',
        fingerprint: 'x',
        trustLevel: 'trusted',
        deviceInfo: {},
        trustExpiresAt: new Date(Date.now() - 86400000).toISOString(),
        createdAt: '2026-01-01T00:00:00Z',
        lastSeenAt: '2026-01-01T00:00:00Z',
      })
    ).toBe(false);
  });
});

// ── SmartAuth event types in @bytelyst/events ───────────────

describe('SmartAuth event schemas', () => {
  it('PlatformEventSchemas includes SmartAuth events', async () => {
    const { PlatformEventSchemas } = await import('@bytelyst/events');
    expect(PlatformEventSchemas['auth.account_locked']).toBeDefined();
    expect(PlatformEventSchemas['auth.oauth_linked']).toBeDefined();
    expect(PlatformEventSchemas['auth.oauth_unlinked']).toBeDefined();
    expect(PlatformEventSchemas['auth.membership_provisioned']).toBeDefined();
    expect(PlatformEventSchemas['auth.account_merged']).toBeDefined();
  });
});
