// @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';

// Minimal user type for testing
interface TestUser {
  email: string;
  name: string;
  role: string;
  [key: string]: unknown;
}

// Mock fetch globally
const mockFetch = vi.fn();
globalThis.fetch = mockFetch;

// localStorage mock
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: 'test',
    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('createAuthProvider', () => {
  beforeEach(() => {
    cleanup();
    localStorageMock.clear();
    vi.clearAllMocks();
    mockFetch.mockReset();
  });

  it('returns AuthProvider and useAuth', () => {
    const result = createTestAuth();
    expect(result.AuthProvider).toBeDefined();
    expect(result.useAuth).toBeDefined();
    expect(typeof result.AuthProvider).toBe('function');
    expect(typeof result.useAuth).toBe('function');
  });

  it('renders children', () => {
    const { AuthProvider } = createTestAuth();
    render(
      <AuthProvider>
        <div data-testid="child">Hello</div>
      </AuthProvider>
    );
    expect(screen.getByTestId('child').textContent).toBe('Hello');
  });

  it('starts unauthenticated with no stored user', () => {
    const { AuthProvider, useAuth } = createTestAuth();
    function Display() {
      const { user, isAuthenticated, isLoading } = useAuth();
      return (
        <div>
          <span data-testid="auth">{String(isAuthenticated)}</span>
          <span data-testid="loading">{String(isLoading)}</span>
          <span data-testid="user">{user ? user.email : 'none'}</span>
        </div>
      );
    }
    render(
      <AuthProvider>
        <Display />
      </AuthProvider>
    );
    expect(screen.getByTestId('auth').textContent).toBe('false');
    expect(screen.getByTestId('loading').textContent).toBe('false');
    expect(screen.getByTestId('user').textContent).toBe('none');
  });

  it('restores user from localStorage on mount', () => {
    const storedUser: TestUser = { email: 'a@b.com', name: 'Stored', role: 'user' };
    store['test_auth_user'] = JSON.stringify(storedUser);

    const { AuthProvider, useAuth } = createTestAuth();
    function Display() {
      const { user, isAuthenticated } = useAuth();
      return (
        <div>
          <span data-testid="auth">{String(isAuthenticated)}</span>
          <span data-testid="email">{user?.email ?? 'none'}</span>
        </div>
      );
    }
    render(
      <AuthProvider>
        <Display />
      </AuthProvider>
    );
    expect(screen.getByTestId('auth').textContent).toBe('true');
    expect(screen.getByTestId('email').textContent).toBe('a@b.com');
  });

  it('login stores user and tokens on success', async () => {
    const apiResponse = {
      user: { email: 'test@example.com', name: 'Test' },
      accessToken: 'at-123',
      refreshToken: 'rt-456',
    };
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => apiResponse,
      headers: new Headers({ 'content-type': 'application/json' }),
      status: 200,
    });

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

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

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

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

    let result: boolean = false;
    await act(async () => {
      result = await loginFn!('test@example.com', 'pass123');
    });

    expect(result).toBe(true);
    expect(screen.getByTestId('auth').textContent).toBe('true');
    expect(screen.getByTestId('email').textContent).toBe('test@example.com');
    expect(mockFetch).toHaveBeenCalledWith(
      '/api/auth/login',
      expect.objectContaining({
        method: 'POST',
        body: JSON.stringify({ email: 'test@example.com', password: 'pass123' }),
      })
    );
    expect(localStorageMock.setItem).toHaveBeenCalledWith(
      'test_auth_user',
      expect.stringContaining('test@example.com')
    );
    expect(localStorageMock.setItem).toHaveBeenCalledWith('test_access_token', 'at-123');
    expect(localStorageMock.setItem).toHaveBeenCalledWith('test_refresh_token', 'rt-456');
  });

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

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

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

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

    let result: boolean = false;
    await act(async () => {
      result = await loginFn!('bad@example.com', 'wrong');
    });

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

  it('logout clears user and storage', async () => {
    store['test_auth_user'] = JSON.stringify({ email: 'a@b.com', name: 'A', role: 'admin' });
    store['test_access_token'] = 'token';
    store['test_refresh_token'] = 'refresh';

    const onLogout = vi.fn();
    const { AuthProvider, useAuth } = createTestAuth({ onLogout });
    let logoutFn: () => void;

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

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

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

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

    expect(screen.getByTestId('auth').textContent).toBe('false');
    expect(localStorageMock.removeItem).toHaveBeenCalledWith('test_auth_user');
    expect(localStorageMock.removeItem).toHaveBeenCalledWith('test_access_token');
    expect(localStorageMock.removeItem).toHaveBeenCalledWith('test_refresh_token');
    expect(onLogout).toHaveBeenCalledOnce();
  });

  it('useAuth throws outside AuthProvider', () => {
    const { useAuth } = createTestAuth();
    function Bad() {
      useAuth();
      return null;
    }
    expect(() => render(<Bad />)).toThrow('useAuth must be used within an AuthProvider');
  });

  it('calls onLoginFallback when API fails', async () => {
    mockFetch.mockRejectedValueOnce(new Error('Network error'));

    const fallbackUser: TestUser = { email: 'mock@test.com', name: 'Mock', role: 'user' };
    const onLoginFallback = vi.fn().mockResolvedValue({
      user: fallbackUser,
      accessToken: 'fallback-at',
      refreshToken: 'fallback-rt',
    });

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

    function Component() {
      const { login, user } = useAuth();
      loginFn = login;
      return <span data-testid="email">{user?.email ?? 'none'}</span>;
    }

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

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

    expect(result).toBe(true);
    expect(onLoginFallback).toHaveBeenCalledWith('mock@test.com', 'pass', expect.any(String));
    expect(screen.getByTestId('email').textContent).toBe('mock@test.com');
  });

  it('updateUser merges partial updates into user state', async () => {
    const apiResponse = {
      user: { email: 'test@example.com', name: 'Original', role: 'user' },
      accessToken: 'at-1',
      refreshToken: 'rt-1',
    };
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => apiResponse,
      headers: new Headers({ 'content-type': 'application/json' }),
      status: 200,
    });

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

    function Component() {
      const { login, updateUser, user } = useAuth();
      loginFn = login;
      updateUserFn = updateUser;
      return (
        <div>
          <span data-testid="name">{user?.name ?? 'none'}</span>
          <span data-testid="role">{user?.role ?? 'none'}</span>
        </div>
      );
    }

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

    await act(async () => {
      await loginFn!('test@example.com', 'pass');
    });
    expect(screen.getByTestId('name').textContent).toBe('Original');

    act(() => {
      updateUserFn!({ name: 'Updated' });
    });

    expect(screen.getByTestId('name').textContent).toBe('Updated');
    expect(screen.getByTestId('role').textContent).toBe('user');
    // Verify localStorage was updated too
    const storedUser = JSON.parse(store['test_auth_user']);
    expect(storedUser.name).toBe('Updated');
    expect(storedUser.role).toBe('user');
  });

  it('updateUser is a no-op when no user is logged in', () => {
    const { AuthProvider, useAuth } = createTestAuth();
    let updateUserFn: (updates: Partial<TestUser>) => void;

    function Component() {
      const { updateUser, user } = useAuth();
      updateUserFn = updateUser;
      return <span data-testid="user">{user ? 'yes' : 'no'}</span>;
    }

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

    act(() => {
      updateUserFn!({ name: 'Should not crash' });
    });

    expect(screen.getByTestId('user').textContent).toBe('no');
  });

  it('onInit provides initial session from external source', () => {
    const initUser: TestUser = { email: 'sso@corp.com', name: 'SSO User', role: 'admin' };
    const { AuthProvider, useAuth } = createTestAuth({
      onInit: () => ({
        user: initUser,
        accessToken: 'sso-at',
        refreshToken: 'sso-rt',
      }),
    });

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

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

    expect(screen.getByTestId('auth').textContent).toBe('true');
    expect(screen.getByTestId('email').textContent).toBe('sso@corp.com');
    // Verify tokens were saved
    expect(store['test_access_token']).toBe('sso-at');
    expect(store['test_refresh_token']).toBe('sso-rt');
  });

  it('onInit returning null falls through to localStorage', () => {
    store['test_auth_user'] = JSON.stringify({
      email: 'stored@local.com',
      name: 'Local',
      role: 'user',
    });

    const { AuthProvider, useAuth } = createTestAuth({
      onInit: () => null,
    });

    function Display() {
      const { user } = useAuth();
      return <span data-testid="email">{user?.email ?? 'none'}</span>;
    }

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

    expect(screen.getByTestId('email').textContent).toBe('stored@local.com');
  });

  it('onInit takes priority over localStorage when it returns a session', () => {
    store['test_auth_user'] = JSON.stringify({
      email: 'stored@local.com',
      name: 'Local',
      role: 'user',
    });

    const { AuthProvider, useAuth } = createTestAuth({
      onInit: () => ({
        user: { email: 'init@override.com', name: 'Init', role: 'admin' },
        accessToken: 'init-at',
        refreshToken: 'init-rt',
      }),
    });

    function Display() {
      const { user } = useAuth();
      return <span data-testid="email">{user?.email ?? 'none'}</span>;
    }

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

    expect(screen.getByTestId('email').textContent).toBe('init@override.com');
  });

  it('uses correct storage prefix for keys', () => {
    const storedUser: TestUser = { email: 'x@y.com', name: 'X', role: 'viewer' };
    store['custom_auth_user'] = JSON.stringify(storedUser);

    const { AuthProvider, useAuth } = createAuthProvider<TestUser>({
      storagePrefix: 'custom',
      loginEndpoint: '/login',
      mapLoginResponse: (d: unknown) =>
        d as { user: TestUser; accessToken: string; refreshToken: string },
    });

    function Display() {
      const { user } = useAuth();
      return <span data-testid="email">{user?.email ?? 'none'}</span>;
    }

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

    expect(screen.getByTestId('email').textContent).toBe('x@y.com');
  });
});
