import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createAuthClient } from '../client.js';
import type { TokenStorage } from '../types.js';

function createMockStorage(): TokenStorage & { store: Map<string, string> } {
  const store = new Map<string, string>();
  return {
    store,
    getItem: (key: string) => store.get(key) ?? null,
    setItem: (key: string, value: string) => store.set(key, value),
    removeItem: (key: string) => store.delete(key),
  };
}

function mockFetchResponse(data: unknown, status = 200) {
  return vi.fn().mockResolvedValue({
    ok: status >= 200 && status < 300,
    status,
    json: () => Promise.resolve(data),
  });
}

describe('@bytelyst/auth-client — SmartAuth', () => {
  let storage: ReturnType<typeof createMockStorage>;

  beforeEach(() => {
    storage = createMockStorage();
    vi.restoreAllMocks();
  });

  // ── Phase 1C: Google Sign-In ──────────────────────

  describe('loginWithGoogle', () => {
    it('calls POST /auth/oauth/google and stores tokens', async () => {
      const mockData = {
        accessToken: 'google-at',
        refreshToken: 'google-rt',
        user: { id: 'u1', email: 'g@gmail.com', displayName: 'G', role: 'user', plan: 'free' },
      };
      globalThis.fetch = mockFetchResponse(mockData);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      const result = await client.loginWithGoogle('google-id-token-123');

      expect('user' in result).toBe(true);
      if ('user' in result) {
        expect(result.user.email).toBe('g@gmail.com');
      }
      expect(client.getAccessToken()).toBe('google-at');
      expect(client.getRefreshToken()).toBe('google-rt');

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/oauth/google');
      expect(opts.method).toBe('POST');
      const body = JSON.parse(opts.body);
      expect(body.idToken).toBe('google-id-token-123');
      expect(body.productId).toBe('testapp');
    });

    it('returns MFA challenge when mfaRequired is true', async () => {
      const mfaData = {
        mfaRequired: true,
        mfaChallenge: 'challenge-token-abc',
        methods: ['totp'],
      };
      globalThis.fetch = mockFetchResponse(mfaData);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      const result = await client.loginWithGoogle('google-id-token-456');

      expect('mfaRequired' in result).toBe(true);
      if ('mfaRequired' in result) {
        expect(result.mfaRequired).toBe(true);
        expect(result.mfaChallenge).toBe('challenge-token-abc');
        expect(result.methods).toEqual(['totp']);
      }
      // Tokens should NOT be stored when MFA is required
      expect(client.isAuthenticated()).toBe(false);
    });
  });

  describe('loginWithMicrosoft', () => {
    it('calls POST /auth/oauth/microsoft', async () => {
      const mockData = {
        accessToken: 'ms-at',
        refreshToken: 'ms-rt',
        user: { id: 'u2', email: 'ms@outlook.com', displayName: 'MS', role: 'user', plan: 'free' },
      };
      globalThis.fetch = mockFetchResponse(mockData);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      await client.loginWithMicrosoft('ms-id-token');

      const [url] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/oauth/microsoft');
      expect(client.getAccessToken()).toBe('ms-at');
    });
  });

  describe('loginWithApple', () => {
    it('calls POST /auth/oauth/apple', async () => {
      const mockData = {
        accessToken: 'apple-at',
        refreshToken: 'apple-rt',
        user: { id: 'u3', email: 'a@icloud.com', displayName: 'A', role: 'user', plan: 'free' },
      };
      globalThis.fetch = mockFetchResponse(mockData);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      await client.loginWithApple('apple-id-token');

      const [url] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/oauth/apple');
      expect(client.getAccessToken()).toBe('apple-at');
    });
  });

  // ── Phase 1C: Provider management ─────────────────

  describe('getProviders', () => {
    it('calls GET /auth/providers', async () => {
      const providers = {
        providers: [
          {
            provider: 'google',
            email: 'g@gmail.com',
            linkedAt: '2025-01-01T00:00:00Z',
            lastUsedAt: null,
          },
        ],
      };
      globalThis.fetch = mockFetchResponse(providers);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      const result = await client.getProviders();

      expect(result).toHaveLength(1);
      expect(result[0].provider).toBe('google');

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/providers');
      expect(opts.method).toBe('GET');
      expect(opts.headers['Authorization']).toBe('Bearer tok');
    });
  });

  describe('linkProvider', () => {
    it('calls POST /auth/providers/link with provider and idToken', async () => {
      globalThis.fetch = mockFetchResponse({});

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      await client.linkProvider('google', 'link-token');

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/providers/link');
      expect(opts.method).toBe('POST');
      const body = JSON.parse(opts.body);
      expect(body.provider).toBe('google');
      expect(body.idToken).toBe('link-token');
    });
  });

  describe('unlinkProvider', () => {
    it('calls DELETE /auth/providers/:provider', async () => {
      globalThis.fetch = mockFetchResponse({});

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      await client.unlinkProvider('google');

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/providers/google');
      expect(opts.method).toBe('DELETE');
    });
  });

  // ── Phase 2D: MFA ─────────────────────────────────

  describe('verifyMfa', () => {
    it('sends challenge token and TOTP code, stores tokens', async () => {
      const mockData = {
        accessToken: 'mfa-at',
        refreshToken: 'mfa-rt',
        user: { id: 'u1', email: 'a@b.com', displayName: 'A', role: 'user', plan: 'free' },
      };
      globalThis.fetch = mockFetchResponse(mockData);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      const result = await client.verifyMfa('challenge-xyz', '123456', 'totp');

      expect(result.user.email).toBe('a@b.com');
      expect(client.getAccessToken()).toBe('mfa-at');

      const body = JSON.parse((globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
      expect(body.challengeToken).toBe('challenge-xyz');
      expect(body.code).toBe('123456');
      expect(body.method).toBe('totp');
    });
  });

  describe('setupTotp', () => {
    it('calls POST /auth/mfa/setup', async () => {
      const setup = {
        secret: 'ABCDEFGH',
        otpauthUri: 'otpauth://totp/Test?secret=ABC',
        recoveryCodes: ['code1', 'code2'],
      };
      globalThis.fetch = mockFetchResponse(setup);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      const result = await client.setupTotp();

      expect(result.otpauthUri).toContain('otpauth://');
      expect(result.secret).toBe('ABCDEFGH');
      expect(result.recoveryCodes).toHaveLength(2);

      const [url] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/mfa/setup');
    });
  });

  describe('getMfaStatus', () => {
    it('returns MFA status', async () => {
      const status = { mfaEnabled: true, methods: ['totp'], recoveryCodesRemaining: 6 };
      globalThis.fetch = mockFetchResponse(status);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      const result = await client.getMfaStatus();
      expect(result.mfaEnabled).toBe(true);
      expect(result.methods).toContain('totp');
    });
  });

  // ── Phase 3: Passkeys ─────────────────────────────

  describe('listPasskeys', () => {
    it('calls GET /auth/passkeys and unwraps response', async () => {
      const data = {
        passkeys: [
          {
            credentialId: 'pk1',
            friendlyName: 'MacBook',
            deviceType: 'singleDevice',
            backedUp: true,
            lastUsedAt: '2025-01-01T00:00:00Z',
            createdAt: '2025-01-01T00:00:00Z',
          },
        ],
      };
      globalThis.fetch = mockFetchResponse(data);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      const result = await client.listPasskeys();
      expect(result).toHaveLength(1);
      expect(result[0].friendlyName).toBe('MacBook');
      expect(result[0].credentialId).toBe('pk1');
    });
  });

  describe('verifyPasskeyAuth', () => {
    it('stores tokens after passkey authentication', async () => {
      const mockData = {
        accessToken: 'pk-at',
        refreshToken: 'pk-rt',
        user: { id: 'u1', email: 'a@b.com', displayName: 'A', role: 'user', plan: 'free' },
      };
      globalThis.fetch = mockFetchResponse(mockData);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      const result = await client.verifyPasskeyAuth({ id: 'cred-1', response: {} });

      expect(result.user.email).toBe('a@b.com');
      expect(client.getAccessToken()).toBe('pk-at');

      const [url] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/passkeys/authenticate/verify');
    });
  });

  // ── Phase 3: Devices ──────────────────────────────

  describe('listDevices', () => {
    it('calls GET /auth/devices and unwraps response', async () => {
      const data = {
        devices: [
          {
            fingerprint: 'fp-abc',
            trustLevel: 'trusted',
            deviceInfo: { platform: 'web' },
            lastIp: '1.2.3.4',
            trustExpiresAt: '2025-04-01T00:00:00Z',
            createdAt: '2025-01-01T00:00:00Z',
            lastSeenAt: '2025-01-01T00:00:00Z',
            isTrusted: true,
          },
        ],
      };
      globalThis.fetch = mockFetchResponse(data);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      const result = await client.listDevices();
      expect(result).toHaveLength(1);
      expect(result[0].trustLevel).toBe('trusted');
      expect(result[0].fingerprint).toBe('fp-abc');
    });
  });

  describe('trustDevice', () => {
    it('calls POST /auth/devices/trust with fingerprint and trustLevel', async () => {
      globalThis.fetch = mockFetchResponse({});

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      await client.trustDevice('fp-abc', 'trusted', { platform: 'web' });

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/devices/trust');
      expect(opts.method).toBe('POST');
      const body = JSON.parse(opts.body);
      expect(body.fingerprint).toBe('fp-abc');
      expect(body.trustLevel).toBe('trusted');
      expect(body.deviceInfo).toEqual({ platform: 'web' });
    });
  });

  describe('revokeDevice', () => {
    it('calls DELETE /auth/devices/:fingerprint', async () => {
      globalThis.fetch = mockFetchResponse({});

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      await client.revokeDevice('fp-xyz');

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/devices/fp-xyz');
      expect(opts.method).toBe('DELETE');
    });
  });

  describe('revokeAllDevices', () => {
    it('calls POST /auth/devices/revoke-all', async () => {
      globalThis.fetch = mockFetchResponse({});

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      await client.revokeAllDevices();

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/devices/revoke-all');
      expect(opts.method).toBe('POST');
    });
  });

  // ── Phase 5B: Admin security ──────────────────────

  describe('getSecurityOverview', () => {
    it('calls GET /auth/security/overview', async () => {
      const overview = {
        totalUsers: 100,
        mfaAdoptionPercent: 42.5,
        providerDistribution: { google: 60, microsoft: 30, password: 10 },
        activeSessions: 250,
        suspiciousEvents24h: 3,
      };
      globalThis.fetch = mockFetchResponse(overview);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('admin-tok', 'admin-ref');

      const result = await client.getSecurityOverview();
      expect(result.totalUsers).toBe(100);
      expect(result.mfaAdoptionPercent).toBe(42.5);
    });
  });

  describe('unlockUser', () => {
    it('calls POST /auth/users/:id/unlock', async () => {
      globalThis.fetch = mockFetchResponse({});

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('admin-tok', 'admin-ref');

      await client.unlockUser('user-locked-123');

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/users/user-locked-123/unlock');
      expect(opts.method).toBe('POST');
    });
  });

  describe('cancelDeletion', () => {
    it('calls POST /auth/account/cancel-deletion', async () => {
      globalThis.fetch = mockFetchResponse({ message: 'Deletion cancelled' });

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      const result = await client.cancelDeletion();
      expect(result.message).toBe('Deletion cancelled');
    });
  });

  // ── login() MFA flow ──────────────────────────────

  describe('login with MFA challenge', () => {
    it('returns MfaLoginResult and does not store tokens', async () => {
      const mfaResponse = {
        mfaRequired: true,
        mfaChallenge: 'login-challenge',
        methods: ['totp', 'recovery'],
      };
      globalThis.fetch = mockFetchResponse(mfaResponse);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      const result = await client.login('user@test.com', 'password');

      expect('mfaRequired' in result).toBe(true);
      if ('mfaRequired' in result) {
        expect(result.mfaChallenge).toBe('login-challenge');
        expect(result.methods).toEqual(['totp', 'recovery']);
      }
      expect(client.isAuthenticated()).toBe(false);
    });
  });

  // ── createAuthClient includes all SmartAuth methods ──

  describe('client exposes all SmartAuth methods', () => {
    it('has all phase 1C, 2D, 3, 5B methods', () => {
      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      // Phase 1C
      expect(client.loginWithGoogle).toBeTypeOf('function');
      expect(client.loginWithMicrosoft).toBeTypeOf('function');
      expect(client.loginWithApple).toBeTypeOf('function');
      expect(client.getProviders).toBeTypeOf('function');
      expect(client.linkProvider).toBeTypeOf('function');
      expect(client.unlinkProvider).toBeTypeOf('function');
      // Phase 2D
      expect(client.verifyMfa).toBeTypeOf('function');
      expect(client.setupTotp).toBeTypeOf('function');
      expect(client.verifyTotpSetup).toBeTypeOf('function');
      expect(client.disableMfa).toBeTypeOf('function');
      expect(client.getMfaStatus).toBeTypeOf('function');
      expect(client.regenerateRecoveryCodes).toBeTypeOf('function');
      // Phase 3
      expect(client.getPasskeyRegisterOptions).toBeTypeOf('function');
      expect(client.verifyPasskeyRegistration).toBeTypeOf('function');
      expect(client.getPasskeyAuthOptions).toBeTypeOf('function');
      expect(client.verifyPasskeyAuth).toBeTypeOf('function');
      expect(client.listPasskeys).toBeTypeOf('function');
      expect(client.deletePasskey).toBeTypeOf('function');
      expect(client.listDevices).toBeTypeOf('function');
      expect(client.trustDevice).toBeTypeOf('function');
      expect(client.revokeDevice).toBeTypeOf('function');
      expect(client.revokeAllDevices).toBeTypeOf('function');
      // Phase 5B
      expect(client.getSecurityOverview).toBeTypeOf('function');
      expect(client.unlockUser).toBeTypeOf('function');
      expect(client.exportAuthData).toBeTypeOf('function');
      expect(client.cancelDeletion).toBeTypeOf('function');
    });
  });
});
