import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';
import { AUTH_BFF_BASE_URL, AUTH_NAVIGATOR, AUTH_PATH_PREFIX } from './auth.config';
import { AuthService } from './auth.service';
import { bffUnauthorizedInterceptor } from './bff-unauthorized.interceptor';

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

function setup() {
  TestBed.configureTestingModule({
    providers: [
      provideHttpClient(withInterceptors([bffUnauthorizedInterceptor])),
      provideHttpClientTesting(),
      { provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE },
      { provide: AUTH_NAVIGATOR, useValue: vi.fn() },
    ],
  });
  return {
    http: TestBed.inject(HttpClient),
    httpCtrl: TestBed.inject(HttpTestingController),
    auth: TestBed.inject(AuthService),
  };
}

async function swallow<T>(p: Promise<T>): Promise<unknown> {
  try {
    return await p;
  } catch (e) {
    return e;
  }
}

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

  it('on 401 from a protected BFF route, fires AuthService.refresh()', async () => {
    const { http, httpCtrl, auth } = setup();
    // AuthService's constructor fires the bootstrap /me — drain it
    // to authenticated so we can observe the refresh triggered by
    // the protected-route 401 below.
    httpCtrl.expectOne(ME_URL).flush({
      oid: 'u',
      tid: 't',
      username: 'jane@apf.example',
      displayName: 'Jane',
    });
    await Promise.resolve();
    expect(auth.state().kind).toBe('authenticated');

    const refreshSpy = vi.spyOn(auth, 'refresh');
    const reqPromise = firstValueFrom(http.get(PROTECTED_URL));
    httpCtrl
      .expectOne(PROTECTED_URL)
      .flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' });

    // The interceptor's refresh() triggers a second /me; respond
    // anonymous so the state transitions.
    httpCtrl
      .expectOne(ME_URL)
      .flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' });

    await swallow(reqPromise);
    expect(refreshSpy).toHaveBeenCalledTimes(1);
    expect(auth.state().kind).toBe('anonymous');
  });

  it('does NOT fire refresh on a 401 from /auth/me itself (would loop)', async () => {
    const { httpCtrl, auth } = setup();
    const refreshSpy = vi.spyOn(auth, 'refresh');
    // Bootstrap /me returns 401.
    httpCtrl
      .expectOne(ME_URL)
      .flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' });
    await Promise.resolve();
    // Interceptor should have stayed quiet — only the explicit
    // AuthService.refresh() call from the constructor ran, which
    // we count separately.
    expect(refreshSpy).not.toHaveBeenCalled();
    expect(auth.state().kind).toBe('anonymous');
  });

  it('does NOT fire refresh on 4xx other than 401', async () => {
    const { http, httpCtrl, auth } = setup();
    httpCtrl
      .expectOne(ME_URL)
      .flush(
        { oid: 'u', tid: 't', username: 'j', displayName: 'J' },
        { status: 200, statusText: 'OK' },
      );
    await Promise.resolve();

    const refreshSpy = vi.spyOn(auth, 'refresh');
    const reqPromise = firstValueFrom(http.get(PROTECTED_URL));
    httpCtrl
      .expectOne(PROTECTED_URL)
      .flush({ error: 'forbidden' }, { status: 403, statusText: 'Forbidden' });

    await swallow(reqPromise);
    expect(refreshSpy).not.toHaveBeenCalled();
    expect(auth.state().kind).toBe('authenticated');
  });

  it('does NOT fire refresh on a 401 from the admin /me when AUTH_PATH_PREFIX is /admin/auth (would loop)', async () => {
    // Regression for PR #134 follow-up: the original interceptor
    // hard-coded `/auth/me` as the skip target. With portal-admin
    // overriding the path prefix to `/admin/auth`, the 401 from
    // `/admin/auth/me` fell through, triggering AuthService.refresh()
    // which re-fired the same request — a tight loop that exhausted
    // the BFF rate limiter on every admin landing.
    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(withInterceptors([bffUnauthorizedInterceptor])),
        provideHttpClientTesting(),
        { provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE },
        { provide: AUTH_PATH_PREFIX, useValue: '/admin/auth' },
        { provide: AUTH_NAVIGATOR, useValue: vi.fn() },
      ],
    });
    const adminMeUrl = `${BFF_BASE}/admin/auth/me`;
    const httpCtrl = TestBed.inject(HttpTestingController);
    const auth = TestBed.inject(AuthService);
    const refreshSpy = vi.spyOn(auth, 'refresh');
    httpCtrl
      .expectOne(adminMeUrl)
      .flush({ error: 'unauthenticated' }, { status: 401, statusText: 'Unauthorized' });
    await Promise.resolve();
    expect(refreshSpy).not.toHaveBeenCalled();
    expect(auth.state().kind).toBe('anonymous');
    // Crucially: no follow-up /me request was issued by the
    // interceptor. If one had been, the testing backend would
    // either record a pending request or this assertion would
    // fail trying to verify().
    httpCtrl.verify();
  });

  it('does NOT touch 401s from non-BFF origins', async () => {
    const { http, httpCtrl, auth } = setup();
    httpCtrl
      .expectOne(ME_URL)
      .flush(
        { oid: 'u', tid: 't', username: 'j', displayName: 'J' },
        { status: 200, statusText: 'OK' },
      );
    await Promise.resolve();

    const refreshSpy = vi.spyOn(auth, 'refresh');
    const reqPromise = firstValueFrom(http.get('http://third-party.example/api'));
    httpCtrl
      .expectOne('http://third-party.example/api')
      .flush({}, { status: 401, statusText: 'Unauthorized' });

    await swallow(reqPromise);
    expect(refreshSpy).not.toHaveBeenCalled();
  });
});
