import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createAuthClient } from '../client.js';
import type { TokenStorage } from '../types.js';

function createMockStorage(): TokenStorage & { store: Map<string, string> } {
  const store = new Map<string, string>();
  return {
    store,
    getItem: (key: string) => store.get(key) ?? null,
    setItem: (key: string, value: string) => store.set(key, value),
    removeItem: (key: string) => store.delete(key),
  };
}

function mockFetchResponse(data: unknown, status = 200) {
  return vi.fn().mockResolvedValue({
    ok: status >= 200 && status < 300,
    status,
    json: () => Promise.resolve(data),
  });
}

describe('@bytelyst/auth-client', () => {
  let storage: ReturnType<typeof createMockStorage>;

  beforeEach(() => {
    storage = createMockStorage();
    vi.restoreAllMocks();
  });

  describe('createAuthClient', () => {
    it('creates a client with all expected methods', () => {
      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      expect(client.login).toBeTypeOf('function');
      expect(client.register).toBeTypeOf('function');
      expect(client.getMe).toBeTypeOf('function');
      expect(client.refreshAccessToken).toBeTypeOf('function');
      expect(client.forgotPassword).toBeTypeOf('function');
      expect(client.resetPassword).toBeTypeOf('function');
      expect(client.changePassword).toBeTypeOf('function');
      expect(client.deleteAccount).toBeTypeOf('function');
      expect(client.verifyEmail).toBeTypeOf('function');
      expect(client.resendVerification).toBeTypeOf('function');
      expect(client.getAccessToken).toBeTypeOf('function');
      expect(client.getRefreshToken).toBeTypeOf('function');
      expect(client.setTokens).toBeTypeOf('function');
      expect(client.clearTokens).toBeTypeOf('function');
      expect(client.isAuthenticated).toBeTypeOf('function');
    });
  });

  describe('token management', () => {
    it('stores and retrieves tokens', () => {
      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      expect(client.isAuthenticated()).toBe(false);
      expect(client.getAccessToken()).toBeNull();

      client.setTokens('access-123', 'refresh-456');

      expect(client.isAuthenticated()).toBe(true);
      expect(client.getAccessToken()).toBe('access-123');
      expect(client.getRefreshToken()).toBe('refresh-456');
    });

    it('clears tokens', () => {
      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      client.setTokens('access-123', 'refresh-456');
      client.clearTokens();

      expect(client.isAuthenticated()).toBe(false);
      expect(client.getAccessToken()).toBeNull();
      expect(client.getRefreshToken()).toBeNull();
    });

    it('uses productId as storage key prefix by default', () => {
      createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'chronomind',
        storage,
      }).setTokens('a', 'b');

      expect(storage.store.get('chronomind-auth-token')).toBe('a');
      expect(storage.store.get('chronomind-refresh-token')).toBe('b');
    });

    it('respects custom storagePrefix', () => {
      createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'chronomind',
        storagePrefix: 'cm',
        storage,
      }).setTokens('a', 'b');

      expect(storage.store.get('cm-auth-token')).toBe('a');
    });
  });

  describe('login', () => {
    it('sends correct request and stores tokens', async () => {
      const mockData = {
        accessToken: 'at-123',
        refreshToken: 'rt-456',
        user: { id: 'u1', email: 'a@b.com', displayName: 'A', role: 'user', plan: 'free' },
      };
      globalThis.fetch = mockFetchResponse(mockData);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      const result = await client.login('a@b.com', 'pass123');

      expect(result.user.email).toBe('a@b.com');
      expect(client.getAccessToken()).toBe('at-123');
      expect(client.getRefreshToken()).toBe('rt-456');

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toBe('http://localhost:4003/api/auth/login');
      expect(opts.method).toBe('POST');
      const body = JSON.parse(opts.body);
      expect(body.email).toBe('a@b.com');
      expect(body.productId).toBe('testapp');

      expect(opts.headers['x-product-id']).toBe('testapp');
      expect(opts.headers['x-request-id']).toBeTruthy();
    });

    it('throws on login failure', async () => {
      globalThis.fetch = mockFetchResponse({ message: 'Invalid credentials' }, 401);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      await expect(client.login('a@b.com', 'wrong')).rejects.toThrow('Invalid credentials');
      expect(client.isAuthenticated()).toBe(false);
    });
  });

  describe('register', () => {
    it('sends correct request and stores tokens', async () => {
      const mockData = {
        accessToken: 'at-new',
        refreshToken: 'rt-new',
        user: { id: 'u2', email: 'new@b.com', displayName: 'New', role: 'user', plan: 'free' },
      };
      globalThis.fetch = mockFetchResponse(mockData);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'nomgap',
        storage,
      });

      const result = await client.register('new@b.com', 'pass1234', 'New User');

      expect(result.user.displayName).toBe('New');
      expect(client.getAccessToken()).toBe('at-new');

      const body = JSON.parse((globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
      expect(body.displayName).toBe('New User');
      expect(body.productId).toBe('nomgap');
    });
  });

  describe('getMe', () => {
    it('sends authorization header', async () => {
      const mockUser = { id: 'u1', email: 'a@b.com', displayName: 'A', role: 'user', plan: 'free' };
      globalThis.fetch = mockFetchResponse(mockUser);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('my-token', 'my-refresh');

      const user = await client.getMe();
      expect(user.email).toBe('a@b.com');

      const opts = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1];
      expect(opts.headers['Authorization']).toBe('Bearer my-token');
    });
  });

  describe('refreshAccessToken', () => {
    it('refreshes and stores new tokens', async () => {
      const mockRefresh = { accessToken: 'at-refreshed', refreshToken: 'rt-refreshed' };
      globalThis.fetch = mockFetchResponse(mockRefresh);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('old-at', 'old-rt');

      const ok = await client.refreshAccessToken();

      expect(ok).toBe(true);
      expect(client.getAccessToken()).toBe('at-refreshed');
      expect(client.getRefreshToken()).toBe('rt-refreshed');
    });

    it('clears tokens on refresh failure', async () => {
      globalThis.fetch = mockFetchResponse({ error: 'expired' }, 401);

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('old-at', 'old-rt');

      const ok = await client.refreshAccessToken();

      expect(ok).toBe(false);
      expect(client.isAuthenticated()).toBe(false);
    });

    it('returns false if no refresh token', async () => {
      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      const ok = await client.refreshAccessToken();
      expect(ok).toBe(false);
    });
  });

  describe('forgotPassword', () => {
    it('sends email and productId', async () => {
      globalThis.fetch = mockFetchResponse({ message: 'Reset email sent' });

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'mindlyst',
        storage,
      });

      const result = await client.forgotPassword('user@test.com');
      expect(result.message).toBe('Reset email sent');

      const body = JSON.parse((globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
      expect(body.email).toBe('user@test.com');
      expect(body.productId).toBe('mindlyst');
    });
  });

  describe('changePassword', () => {
    it('sends authenticated request', async () => {
      globalThis.fetch = mockFetchResponse({ message: 'Password changed' });

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      const result = await client.changePassword('old', 'new12345');
      expect(result.message).toBe('Password changed');

      const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(opts.headers['Authorization']).toBe('Bearer tok');
      const body = JSON.parse(opts.body);
      expect(body.currentPassword).toBe('old');
      expect(body.newPassword).toBe('new12345');
    });
  });

  describe('deleteAccount', () => {
    it('clears tokens after deletion', async () => {
      globalThis.fetch = mockFetchResponse({ message: 'Deleted' });

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });
      client.setTokens('tok', 'ref');

      await client.deleteAccount('mypassword');

      expect(client.isAuthenticated()).toBe(false);

      const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
      expect(url).toContain('/auth/account');
      expect(opts.method).toBe('DELETE');
    });
  });

  describe('verifyEmail', () => {
    it('sends verification token', async () => {
      globalThis.fetch = mockFetchResponse({ message: 'Verified' });

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'testapp',
        storage,
      });

      const result = await client.verifyEmail('verify-token-abc');
      expect(result.message).toBe('Verified');
    });
  });

  describe('resendVerification', () => {
    it('sends email and productId', async () => {
      globalThis.fetch = mockFetchResponse({ message: 'Sent' });

      const client = createAuthClient({
        baseUrl: 'http://localhost:4003/api',
        productId: 'chronomind',
        storage,
      });

      await client.resendVerification('user@test.com');

      const body = JSON.parse((globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
      expect(body.email).toBe('user@test.com');
      expect(body.productId).toBe('chronomind');
    });
  });
});
