import { TestBed } from '@angular/core/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AuthService } from './auth.service';

describe('AuthService', () => {
  beforeEach(() => {
    sessionStorage.clear();
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('starts unauthenticated when no session is stored', () => {
    const service = TestBed.inject(AuthService);
    expect(service.isAuthenticated()).toBe(false);
  });

  it('starts authenticated when a non-expired access token is already stored', () => {
    sessionStorage.setItem('auth.accessToken', 'stored-token');
    sessionStorage.setItem('auth.expiresAt', String(Date.now() + 60_000));
    const service = TestBed.inject(AuthService);
    expect(service.isAuthenticated()).toBe(true);
  });

  it('login() persists a PKCE verifier and state before redirecting', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({ authorization_endpoint: 'https://idp.example.test/oidc/auth' }),
    });
    vi.stubGlobal('fetch', fetchMock);
    vi.stubGlobal('location', { ...window.location, href: '' });

    const service = TestBed.inject(AuthService);
    await service.login();

    expect(sessionStorage.getItem('auth.codeVerifier')).toBeTruthy();
    expect(sessionStorage.getItem('auth.state')).toBeTruthy();
    expect(window.location.href).toContain('https://idp.example.test/oidc/auth?');
    expect(window.location.href).toContain('code_challenge_method=S256');
  });

  it('completeLogin() rejects a state that does not match the one stored before redirecting', async () => {
    sessionStorage.setItem('auth.codeVerifier', 'verifier-1');
    sessionStorage.setItem('auth.state', 'expected-state');
    vi.stubGlobal('location', { ...window.location, search: '?code=abc&state=wrong-state' });

    const service = TestBed.inject(AuthService);
    await expect(service.completeLogin()).rejects.toThrow();
  });

  it('completeLogin() exchanges the code via the backend and stores the resulting access token', async () => {
    sessionStorage.setItem('auth.codeVerifier', 'verifier-1');
    sessionStorage.setItem('auth.state', 'state-1');
    vi.stubGlobal('location', { ...window.location, search: '?code=abc&state=state-1' });

    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ accessToken: 'at-1', expiresIn: 3600 }),
    });
    vi.stubGlobal('fetch', fetchMock);

    const service = TestBed.inject(AuthService);
    await service.completeLogin();

    expect(service.isAuthenticated()).toBe(true);
    await expect(service.getAccessToken()).resolves.toBe('at-1');
    expect(sessionStorage.getItem('auth.codeVerifier')).toBeNull();
  });

  it('logout() clears the stored session', () => {
    sessionStorage.setItem('auth.accessToken', 'at-1');
    sessionStorage.setItem('auth.expiresAt', String(Date.now() + 60_000));
    const service = TestBed.inject(AuthService);

    service.logout();

    expect(service.isAuthenticated()).toBe(false);
  });

  it('getAccessToken() returns undefined once the token has expired', async () => {
    sessionStorage.setItem('auth.accessToken', 'at-1');
    sessionStorage.setItem('auth.expiresAt', String(Date.now() - 1000));
    const service = TestBed.inject(AuthService);

    await expect(service.getAccessToken()).resolves.toBeUndefined();
  });
});
