import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { AUTH_BFF_BASE_URL, AUTH_NAVIGATOR, AUTH_PATH_PREFIX } from './auth.config';
import { AuthService } from './auth.service';
import type { CurrentUser } from './auth.types';

const BFF_BASE = 'http://bff.test/api';
const ME_URL = `${BFF_BASE}/auth/me`;

const USER: CurrentUser = {
  oid: 'user-oid',
  tid: 'tenant-id',
  username: 'jane.doe@apf.example',
  displayName: 'Jane Doe',
};

interface Fixture {
  service: AuthService;
  http: HttpTestingController;
  navigate: ReturnType<typeof vi.fn>;
}

function setup(): Fixture {
  const navigate = vi.fn();
  TestBed.configureTestingModule({
    providers: [
      provideHttpClient(),
      provideHttpClientTesting(),
      { provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE },
      { provide: AUTH_NAVIGATOR, useValue: navigate },
      AuthService,
    ],
  });
  const service = TestBed.inject(AuthService);
  const http = TestBed.inject(HttpTestingController);
  return { service, http, navigate };
}

describe('AuthService', () => {
  afterEach(() => {
    TestBed.resetTestingModule();
  });

  describe('bootstrap fetch', () => {
    it('starts in the loading state before /me resolves', () => {
      const { service, http } = setup();
      expect(service.state().kind).toBe('loading');
      expect(service.isLoading()).toBe(true);
      expect(service.currentUser()).toBeNull();
      // Drain the in-flight bootstrap request to keep the controller clean.
      http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' });
    });

    it('transitions to authenticated when /me returns the user', async () => {
      const { service, http } = setup();
      http.expectOne(ME_URL).flush(USER);
      // Let the awaited firstValueFrom commit the state.
      await Promise.resolve();
      expect(service.state()).toEqual({ kind: 'authenticated', user: USER });
      expect(service.currentUser()).toEqual(USER);
      expect(service.isLoading()).toBe(false);
    });

    it('transitions to anonymous on 401', async () => {
      const { service, http } = setup();
      http
        .expectOne(ME_URL)
        .flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' });
      await Promise.resolve();
      expect(service.state().kind).toBe('anonymous');
      expect(service.currentUser()).toBeNull();
    });

    it('transitions to error on a non-401 failure (network / 5xx / malformed)', async () => {
      const { service, http } = setup();
      http.expectOne(ME_URL).flush('boom', { status: 500, statusText: 'Internal Server Error' });
      await Promise.resolve();
      expect(service.state().kind).toBe('error');
      expect(service.currentUser()).toBeNull();
    });
  });

  describe('refresh()', () => {
    it('can be called again after the bootstrap fetch and re-fetches /me', async () => {
      const { service, http } = setup();
      http.expectOne(ME_URL).flush(USER);
      await Promise.resolve();
      expect(service.state().kind).toBe('authenticated');

      const promise = service.refresh();
      http
        .expectOne(ME_URL)
        .flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' });
      await promise;
      expect(service.state().kind).toBe('anonymous');
    });
  });

  describe('URL accessors', () => {
    it('derives me / login / logout URLs from the injected base + default /auth prefix', () => {
      const { service, http } = setup();
      expect(service.meUrl).toBe(`${BFF_BASE}/auth/me`);
      expect(service.loginUrl).toBe(`${BFF_BASE}/auth/login`);
      expect(service.logoutUrl).toBe(`${BFF_BASE}/auth/logout`);
      http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' });
    });

    it('honours AUTH_PATH_PREFIX when the host provides a non-default value (admin surface)', () => {
      const navigate = vi.fn();
      TestBed.configureTestingModule({
        providers: [
          provideHttpClient(),
          provideHttpClientTesting(),
          { provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE },
          { provide: AUTH_NAVIGATOR, useValue: navigate },
          { provide: AUTH_PATH_PREFIX, useValue: '/admin/auth' },
          AuthService,
        ],
      });
      const service = TestBed.inject(AuthService);
      const http = TestBed.inject(HttpTestingController);
      expect(service.meUrl).toBe(`${BFF_BASE}/admin/auth/me`);
      expect(service.loginUrl).toBe(`${BFF_BASE}/admin/auth/login`);
      expect(service.logoutUrl).toBe(`${BFF_BASE}/admin/auth/logout`);
      // The auto-refresh on first inject fires against the override path.
      http
        .expectOne(`${BFF_BASE}/admin/auth/me`)
        .flush({}, { status: 401, statusText: 'Unauthorized' });
    });
  });

  describe('login() / logout()', () => {
    it('navigates to /auth/login via AUTH_NAVIGATOR', () => {
      const { service, http, navigate } = setup();
      service.login();
      expect(navigate).toHaveBeenCalledWith(`${BFF_BASE}/auth/login`);
      http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' });
    });

    it('navigates to /auth/logout via AUTH_NAVIGATOR', () => {
      const { service, http, navigate } = setup();
      service.logout();
      expect(navigate).toHaveBeenCalledWith(`${BFF_BASE}/auth/logout`);
      http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' });
    });
  });
});
