import {
  describe,
  it,
  expect,
  beforeEach,
  vi
} from 'vitest';
import { of, throwError } from 'rxjs';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { SignalServerProvisionerService } from './signal-server-provisioner.service';
import { AuthTokenStoreService } from './auth-token-store.service';
import { SignalServerCredentialStoreService } from './signal-server-credential-store.service';
import { ProvisionUsernameCollisionError } from '../../domain/logic/signal-server-provision.rules';
import type { User } from '../../../../shared-kernel';

describe('SignalServerProvisionerService', () => {
  let service: SignalServerProvisionerService;
  let httpPost: ReturnType<typeof vi.fn>;
  let credentialStore: SignalServerCredentialStoreService;

  const homeUser: User = {
    id: 'a3f2b1c4-5678-90ab-cdef-1234567890ab',
    oderId: 'a3f2b1c4-5678-90ab-cdef-1234567890ab',
    username: 'alice',
    displayName: 'Alice',
    status: 'online',
    role: 'member',
    joinedAt: Date.now(),
    homeSignalServerUrl: 'https://home.example.com'
  };

  beforeEach(() => {
    const storage = new Map<string, string>();

    vi.stubGlobal('localStorage', {
      getItem: (key: string) => storage.get(key) ?? null,
      setItem: (key: string, value: string) => { storage.set(key, value); },
      removeItem: (key: string) => { storage.delete(key); },
      clear: () => { storage.clear(); }
    });

    httpPost = vi.fn();
    credentialStore = new SignalServerCredentialStoreService(new AuthTokenStoreService());
    service = new SignalServerProvisionerService(
      { post: httpPost } as unknown as HttpClient,
      credentialStore
    );
  });

  it('registers on a foreign server when the preferred username is available', async () => {
    httpPost.mockReturnValue(of({
      id: 'foreign-user-1',
      username: 'alice',
      displayName: 'Alice',
      token: 'foreign-token',
      expiresAt: Date.now() + 60_000
    }));

    const result = await service.provisionOnServer({
      serverUrl: 'https://foreign.example.com',
      homeUser,
      provisionSecret: 'provision-secret'
    });

    expect(result.username).toBe('alice');
    expect(result.usedSuffix).toBe(false);
    expect(credentialStore.getCredential('https://foreign.example.com')?.userId).toBe('foreign-user-1');
    expect(httpPost).toHaveBeenCalledWith(
      'https://foreign.example.com/api/users/register',
      {
        username: 'alice',
        password: 'provision-secret',
        displayName: 'Alice'
      }
    );
  });

  it('logs in when the preferred username was provisioned earlier', async () => {
    httpPost
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 409 })))
      .mockReturnValueOnce(of({
        id: 'foreign-user-1',
        username: 'alice',
        displayName: 'Alice',
        token: 'foreign-token',
        expiresAt: Date.now() + 60_000
      }));

    const result = await service.provisionOnServer({
      serverUrl: 'https://foreign.example.com',
      homeUser,
      provisionSecret: 'provision-secret'
    });

    expect(result.username).toBe('alice');
    expect(httpPost).toHaveBeenNthCalledWith(
      2,
      'https://foreign.example.com/api/users/login',
      {
        username: 'alice',
        password: 'provision-secret'
      }
    );
  });

  it('registers with a suffixed username when the preferred name belongs to someone else', async () => {
    httpPost
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 409 })))
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 401 })))
      .mockReturnValueOnce(of({
        id: 'foreign-user-2',
        username: 'alice-a3f2b1',
        displayName: 'Alice',
        token: 'foreign-token-2',
        expiresAt: Date.now() + 60_000
      }));

    const result = await service.provisionOnServer({
      serverUrl: 'https://foreign.example.com',
      homeUser,
      provisionSecret: 'provision-secret'
    });

    expect(result.username).toBe('alice-a3f2b1');
    expect(result.usedSuffix).toBe(true);
    expect(httpPost).toHaveBeenNthCalledWith(
      3,
      'https://foreign.example.com/api/users/register',
      {
        username: 'alice-a3f2b1',
        password: 'provision-secret',
        displayName: 'Alice'
      }
    );
  });

  it('throws when all username candidates are exhausted', async () => {
    httpPost
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 409 })))
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 401 })))
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 409 })))
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 401 })));

    await expect(service.provisionOnServer({
      serverUrl: 'https://foreign.example.com',
      homeUser,
      provisionSecret: 'provision-secret'
    })).rejects.toBeInstanceOf(ProvisionUsernameCollisionError);
  });

  it('returns an existing credential without making network calls', async () => {
    credentialStore.upsertCredential({
      serverUrl: 'https://foreign.example.com',
      userId: 'foreign-user-1',
      username: 'alice',
      displayName: 'Alice',
      token: 'foreign-token',
      expiresAt: Date.now() + 60_000,
      provisioned: true
    });

    const result = await service.provisionOnServer({
      serverUrl: 'https://foreign.example.com',
      homeUser,
      provisionSecret: 'provision-secret'
    });

    expect(result.username).toBe('alice');
    expect(httpPost).not.toHaveBeenCalled();
  });
});
