// @vitest-environment happy-dom
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { render, screen, act, cleanup } from '@testing-library/react';
import { createAuthProvider } from '../index.js';

interface TestUser {
  email: string;
  name: string;
  role: string;
  [key: string]: unknown;
}

const mockFetch = vi.fn();
globalThis.fetch = mockFetch;

const store: Record<string, string> = {};
const localStorageMock = {
  getItem: vi.fn((key: string) => store[key] ?? null),
  setItem: vi.fn((key: string, value: string) => {
    store[key] = value;
  }),
  removeItem: vi.fn((key: string) => {
    delete store[key];
  }),
  clear: vi.fn(() => {
    for (const key of Object.keys(store)) delete store[key];
  }),
  length: 0,
  key: vi.fn(),
};
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock });

function createTestAuth(overrides?: Partial<Parameters<typeof createAuthProvider<TestUser>>[0]>) {
  return createAuthProvider<TestUser>({
    storagePrefix: 'sa',
    loginEndpoint: '/auth/login',
    mapLoginResponse: (data: unknown) => {
      const d = data as { user: TestUser; accessToken: string; refreshToken: string };
      return { user: d.user, accessToken: d.accessToken, refreshToken: d.refreshToken };
    },
    ...overrides,
  });
}

describe('react-auth SmartAuth features', () => {
  beforeEach(() => {
    cleanup();
    localStorageMock.clear();
    vi.clearAllMocks();
    mockFetch.mockReset();
  });

  // ── Phase 1C: loginWithGoogle ──────────────────────

  it('loginWithGoogle sets user on success', async () => {
    const apiResponse = {
      user: { email: 'g@gmail.com', name: 'Google User', role: 'user' },
      accessToken: 'g-at',
      refreshToken: 'g-rt',
    };
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => apiResponse,
      headers: new Headers({ 'content-type': 'application/json' }),
      status: 200,
    });

    const { AuthProvider, useAuth } = createTestAuth();
    let loginWithGoogleFn: (idToken: string) => Promise<boolean>;

    function Component() {
      const { loginWithGoogle, user, isAuthenticated } = useAuth();
      loginWithGoogleFn = loginWithGoogle;
      return (
        <div>
          <span data-testid="auth">{String(isAuthenticated)}</span>
          <span data-testid="email">{user?.email ?? 'none'}</span>
        </div>
      );
    }

    render(
      <AuthProvider>
        <Component />
      </AuthProvider>
    );

    expect(screen.getByTestId('auth').textContent).toBe('false');

    let result = false;
    await act(async () => {
      result = await loginWithGoogleFn!('google-id-token');
    });

    expect(result).toBe(true);
    expect(screen.getByTestId('auth').textContent).toBe('true');
    expect(screen.getByTestId('email').textContent).toBe('g@gmail.com');
    expect(localStorageMock.setItem).toHaveBeenCalledWith('sa_access_token', 'g-at');
  });

  it('loginWithGoogle returns false on API failure', async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 401,
      json: async () => ({ error: 'Invalid token' }),
      headers: new Headers({ 'content-type': 'application/json' }),
    });

    const { AuthProvider, useAuth } = createTestAuth();
    let loginWithGoogleFn: (idToken: string) => Promise<boolean>;

    function Component() {
      const { loginWithGoogle, isAuthenticated, error } = useAuth();
      loginWithGoogleFn = loginWithGoogle;
      return (
        <div>
          <span data-testid="auth">{String(isAuthenticated)}</span>
          <span data-testid="error">{error ?? 'none'}</span>
        </div>
      );
    }

    render(
      <AuthProvider>
        <Component />
      </AuthProvider>
    );

    let result = true;
    await act(async () => {
      result = await loginWithGoogleFn!('bad-token');
    });

    expect(result).toBe(false);
    expect(screen.getByTestId('auth').textContent).toBe('false');
  });

  // ── Phase 2D: MFA challenge flow ──────────────────

  it('login triggers MFA state when mfaRequired returned', async () => {
    const mfaResponse = {
      mfaRequired: true,
      mfaChallenge: 'challenge-xyz',
      methods: ['totp'],
    };
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mfaResponse,
      headers: new Headers({ 'content-type': 'application/json' }),
      status: 200,
    });

    const onMfaRequired = vi.fn();
    const { AuthProvider, useAuth } = createTestAuth({ onMfaRequired });
    let loginFn: (email: string, password: string) => Promise<boolean>;

    function Component() {
      const { login, mfaRequired, mfaChallenge, mfaMethods, isAuthenticated } = useAuth();
      loginFn = login;
      return (
        <div>
          <span data-testid="auth">{String(isAuthenticated)}</span>
          <span data-testid="mfa">{String(mfaRequired)}</span>
          <span data-testid="challenge">{mfaChallenge ?? 'none'}</span>
          <span data-testid="methods">{mfaMethods.join(',') || 'none'}</span>
        </div>
      );
    }

    render(
      <AuthProvider>
        <Component />
      </AuthProvider>
    );

    let result = true;
    await act(async () => {
      result = await loginFn!('user@test.com', 'pass');
    });

    // Login returns false (MFA required, not yet authenticated)
    expect(result).toBe(false);
    expect(screen.getByTestId('auth').textContent).toBe('false');
    expect(screen.getByTestId('mfa').textContent).toBe('true');
    expect(screen.getByTestId('challenge').textContent).toBe('challenge-xyz');
    expect(screen.getByTestId('methods').textContent).toBe('totp');
    expect(onMfaRequired).toHaveBeenCalledWith('challenge-xyz', ['totp']);
  });

  it('verifyMfa completes login after MFA challenge', async () => {
    // Step 1: login triggers MFA
    const mfaResponse = {
      mfaRequired: true,
      mfaChallenge: 'challenge-abc',
      methods: ['totp'],
    };
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mfaResponse,
      headers: new Headers({ 'content-type': 'application/json' }),
      status: 200,
    });

    const { AuthProvider, useAuth } = createTestAuth();
    let loginFn: (email: string, password: string) => Promise<boolean>;
    let verifyMfaFn: (code: string, method: 'totp' | 'recovery') => Promise<boolean>;

    function Component() {
      const { login, verifyMfa, mfaRequired, isAuthenticated, user } = useAuth();
      loginFn = login;
      verifyMfaFn = verifyMfa;
      return (
        <div>
          <span data-testid="auth">{String(isAuthenticated)}</span>
          <span data-testid="mfa">{String(mfaRequired)}</span>
          <span data-testid="email">{user?.email ?? 'none'}</span>
        </div>
      );
    }

    render(
      <AuthProvider>
        <Component />
      </AuthProvider>
    );

    await act(async () => {
      await loginFn!('user@test.com', 'pass');
    });

    expect(screen.getByTestId('mfa').textContent).toBe('true');
    expect(screen.getByTestId('auth').textContent).toBe('false');

    // Step 2: verify MFA
    const verifyResponse = {
      user: { email: 'user@test.com', name: 'Test', role: 'user' },
      accessToken: 'mfa-at',
      refreshToken: 'mfa-rt',
    };
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => verifyResponse,
      headers: new Headers({ 'content-type': 'application/json' }),
      status: 200,
    });

    let verifyResult = false;
    await act(async () => {
      verifyResult = await verifyMfaFn!('123456', 'totp');
    });

    expect(verifyResult).toBe(true);
    expect(screen.getByTestId('auth').textContent).toBe('true');
    expect(screen.getByTestId('mfa').textContent).toBe('false');
    expect(screen.getByTestId('email').textContent).toBe('user@test.com');
    expect(localStorageMock.setItem).toHaveBeenCalledWith('sa_access_token', 'mfa-at');
  });

  // ── Phase 1C: Provider management ─────────────────

  it('providers starts empty and exposes link/unlink', () => {
    const { AuthProvider, useAuth } = createTestAuth();

    function Component() {
      const { providers, linkProvider, unlinkProvider, refreshProviders } = useAuth();
      return (
        <div>
          <span data-testid="count">{providers.length}</span>
          <span data-testid="hasLink">{String(typeof linkProvider === 'function')}</span>
          <span data-testid="hasUnlink">{String(typeof unlinkProvider === 'function')}</span>
          <span data-testid="hasRefresh">{String(typeof refreshProviders === 'function')}</span>
        </div>
      );
    }

    render(
      <AuthProvider>
        <Component />
      </AuthProvider>
    );

    expect(screen.getByTestId('count').textContent).toBe('0');
    expect(screen.getByTestId('hasLink').textContent).toBe('true');
    expect(screen.getByTestId('hasUnlink').textContent).toBe('true');
    expect(screen.getByTestId('hasRefresh').textContent).toBe('true');
  });

  // ── Logout clears SmartAuth state ─────────────────

  it('logout clears providers and MFA state', async () => {
    // Login first
    const loginResponse = {
      user: { email: 'a@b.com', name: 'A', role: 'user' },
      accessToken: 'at',
      refreshToken: 'rt',
    };
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => loginResponse,
      headers: new Headers({ 'content-type': 'application/json' }),
      status: 200,
    });

    const { AuthProvider, useAuth } = createTestAuth();
    let loginFn: (email: string, password: string) => Promise<boolean>;
    let logoutFn: () => void;

    function Component() {
      const { login, logout, isAuthenticated, mfaRequired } = useAuth();
      loginFn = login;
      logoutFn = logout;
      return (
        <div>
          <span data-testid="auth">{String(isAuthenticated)}</span>
          <span data-testid="mfa">{String(mfaRequired)}</span>
        </div>
      );
    }

    render(
      <AuthProvider>
        <Component />
      </AuthProvider>
    );

    await act(async () => {
      await loginFn!('a@b.com', 'pass');
    });

    expect(screen.getByTestId('auth').textContent).toBe('true');

    act(() => {
      logoutFn!();
    });

    expect(screen.getByTestId('auth').textContent).toBe('false');
    expect(screen.getByTestId('mfa').textContent).toBe('false');
  });
});
