import { type OAuthService } from '../services/oauth.service';
import { type TokenPair } from '../services/token.service';
import { ZaloOAuthStrategy } from '../strategies/zalo-oauth.strategy';

describe('ZaloOAuthStrategy', () => {
  const mockTokenPair: TokenPair = {
    accessToken: 'test-jwt',
    refreshToken: 'family.token',
    expiresIn: 900,
  };

  let mockOAuthService: { authenticateOAuth: ReturnType<typeof vi.fn> };
  let strategy: ZaloOAuthStrategy;

  beforeEach(() => {
    vi.stubEnv('ZALO_APP_ID', 'test-zalo-app-id');
    vi.stubEnv('ZALO_APP_SECRET', 'test-zalo-secret');
    vi.stubEnv('ZALO_CALLBACK_URL', 'http://localhost:3001/auth/zalo/callback');

    mockOAuthService = {
      authenticateOAuth: vi.fn().mockResolvedValue(mockTokenPair),
    };

    const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
    strategy = new ZaloOAuthStrategy(mockOAuthService as unknown as OAuthService, mockLogger as any);
  });

  afterEach(() => {
    vi.unstubAllEnvs();
    vi.restoreAllMocks();
  });

  it('creates strategy with dummy config when ZALO_APP_ID is missing', () => {
    vi.stubEnv('ZALO_APP_ID', '');
    const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
    const strategy = new ZaloOAuthStrategy(mockOAuthService as unknown as OAuthService, mockLogger as any);
    expect(strategy).toBeDefined();
  });

  it('creates strategy with dummy config when ZALO_APP_SECRET is missing', () => {
    vi.stubEnv('ZALO_APP_SECRET', '');
    const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() };
    const strategy = new ZaloOAuthStrategy(mockOAuthService as unknown as OAuthService, mockLogger as any);
    expect(strategy).toBeDefined();
  });

  describe('getAuthorizationUrl', () => {
    it('returns correct Zalo authorization URL', () => {
      const url = strategy.getAuthorizationUrl('test-state');

      expect(url).toContain('https://oauth.zaloapp.com/v4/permission');
      expect(url).toContain('app_id=test-zalo-app-id');
      expect(url).toContain('redirect_uri=');
      expect(url).toContain('state=test-state');
    });

    it('works without state parameter', () => {
      const url = strategy.getAuthorizationUrl();
      expect(url).toContain('https://oauth.zaloapp.com/v4/permission');
      expect(url).toContain('app_id=test-zalo-app-id');
    });
  });

  describe('handleCallback', () => {
    it('exchanges code, fetches user info, and returns tokens', async () => {
      // Mock fetch for token exchange
      const mockFetch = vi.fn()
        .mockResolvedValueOnce({
          json: () => Promise.resolve({
            access_token: 'zalo-access-token',
            refresh_token: 'zalo-refresh-token',
            expires_in: 3600,
          }),
        })
        // Mock fetch for user info
        .mockResolvedValueOnce({
          json: () => Promise.resolve({
            id: 'zalo-user-123',
            name: 'Nguyen Van A',
            picture: { data: { url: 'https://zalo.me/avatar.jpg' } },
          }),
        });

      vi.stubGlobal('fetch', mockFetch);

      const result = await strategy.handleCallback('auth-code-123');

      expect(result.tokens).toEqual(mockTokenPair);
      expect(mockOAuthService.authenticateOAuth).toHaveBeenCalledWith(
        expect.objectContaining({
          provider: 'ZALO',
          providerUserId: 'zalo-user-123',
          fullName: 'Nguyen Van A',
          accessToken: 'zalo-access-token',
          refreshToken: 'zalo-refresh-token',
        }),
      );

      // Verify token exchange request
      expect(mockFetch).toHaveBeenCalledWith(
        'https://oauth.zaloapp.com/v4/access_token',
        expect.objectContaining({
          method: 'POST',
          headers: expect.objectContaining({
            'secret_key': 'test-zalo-secret',
          }),
        }),
      );

      // Verify user info request
      expect(mockFetch).toHaveBeenCalledWith(
        'https://graph.zalo.me/v2.0/me?fields=id,name,picture',
        expect.objectContaining({
          headers: expect.objectContaining({
            'access_token': 'zalo-access-token',
          }),
        }),
      );
    });

    it('throws on Zalo token exchange error', async () => {
      vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
        json: () => Promise.resolve({
          error: -216,
          error_description: 'Invalid authorization code',
        }),
      }));

      await expect(strategy.handleCallback('bad-code')).rejects.toThrow(
        'Zalo OAuth error: Invalid authorization code',
      );
    });

    it('throws when Zalo returns no access token', async () => {
      vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
        json: () => Promise.resolve({}),
      }));

      await expect(strategy.handleCallback('code')).rejects.toThrow(
        'No access token',
      );
    });

    it('throws on Zalo user info error', async () => {
      const mockFetch = vi.fn()
        .mockResolvedValueOnce({
          json: () => Promise.resolve({
            access_token: 'token',
            expires_in: 3600,
          }),
        })
        .mockResolvedValueOnce({
          json: () => Promise.resolve({
            error: -401,
            message: 'Unauthorized',
          }),
        });

      vi.stubGlobal('fetch', mockFetch);

      await expect(strategy.handleCallback('code')).rejects.toThrow(
        'Zalo OAuth error: Unauthorized',
      );
    });

    it('passes code_verifier when provided', async () => {
      const mockFetch = vi.fn()
        .mockResolvedValueOnce({
          json: () => Promise.resolve({
            access_token: 'token',
            expires_in: 3600,
          }),
        })
        .mockResolvedValueOnce({
          json: () => Promise.resolve({ id: 'z-1', name: 'User' }),
        });

      vi.stubGlobal('fetch', mockFetch);

      await strategy.handleCallback('code', 'pkce-verifier');

      const tokenBody = mockFetch.mock.calls[0][1].body as string;
      expect(tokenBody).toContain('code_verifier=pkce-verifier');
    });
  });
});
