/**
 * Unit tests for the auth store (U7 refactor).
 *
 * Covers:
 *  - 3-state startup (valid / invalid / error / pending)
 *  - Access token in memory only (never written to localStorage)
 *  - Refresh token via tauriAuthStorage (Tauri Keychain or localStorage)
 *  - Pre-emptive refresh trigger (`shouldRefresh`)
 *  - Silent refresh dedup (concurrent callers)
 *  - Logout / logoutLocal clear all sensitive state
 */

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'

// Mock the auth API client so we don't touch the network.
const mockAuthApi = {
  login: vi.fn(),
  refresh: vi.fn(),
  logout: vi.fn(),
  whoami: vi.fn(),
  me: vi.fn(),
  listSessions: vi.fn(),
  revokeSession: vi.fn(),
  changePassword: vi.fn(),
}
vi.mock('@/api/auth', () => ({
  authApi: mockAuthApi,
  AuthApiClient: vi.fn(),
  // Re-export the types so the store's imports don't break
  IAuthUser: {},
  ITokenPair: {},
  ISessionInfo: {},
  IWhoamiResponse: {},
}))

// Mock the tauri auth storage so we don't touch the Keychain.
const mockTauriStorage = {
  setRefreshToken: vi.fn(),
  getRefreshToken: vi.fn(),
  clearRefreshToken: vi.fn(),
}
vi.mock('@/api/tauri-auth', () => ({
  tauriAuthStorage: mockTauriStorage,
  isTauri: () => false,
}))

// Now import the SUT.
const { useAuthStore } = await import('@/stores/auth')

/** A small in-memory localStorage. */
const _store = new Map<string, string>()
const _localStoragePolyfill: Storage = {
  get length() {
    return _store.size
  },
  clear() {
    _store.clear()
  },
  getItem(key: string) {
    return _store.has(key) ? (_store.get(key) as string) : null
  },
  key(index: number) {
    return Array.from(_store.keys())[index] ?? null
  },
  removeItem(key: string) {
    _store.delete(key)
  },
  setItem(key: string, value: string) {
    _store.set(key, String(value))
  },
}

const fakeUser = {
  id: 'user-1',
  username: 'alice',
  email: 'alice@example.com',
  role: 'admin',
  is_active: true,
  is_terminal_authorized: true,
  is_server_terminal_authorized: true,
}

const fakeTokenPair = {
  access_token: 'access.aaa.bbb',
  refresh_token: 'refresh.xxx.yyy',
  token_type: 'bearer' as const,
  expires_in: 900,
  user: fakeUser,
  session_id: 'sid-1',
}

const fakeWhoamiResponse = {
  user: fakeUser,
  access_token: 'access.aaa.bbb',
  session: {
    id: 'sid-1',
    device_fingerprint: 'fp',
    device_label: 'macOS — Chrome',
    ip: '127.0.0.1',
    user_agent: 'jest',
    auth_provider: 'local',
    created_at: '2026-06-20T00:00:00Z',
    last_active_at: '2026-06-20T00:00:00Z',
    expires_at: '2026-06-27T00:00:00Z',
    is_current: true,
  },
}

beforeEach(() => {
  ;(globalThis as { localStorage?: Storage }).localStorage = _localStoragePolyfill
  _store.clear()
  setActivePinia(createPinia())
  // Reset all mocks.
  for (const fn of Object.values(mockAuthApi)) fn.mockReset()
  for (const fn of Object.values(mockTauriStorage)) fn.mockReset()
})

afterEach(() => {
  _store.clear()
  vi.restoreAllMocks()
})

describe('auth store — initial state', () => {
  it('starts in "pending" state', () => {
    const store = useAuthStore()
    expect(store.startupState).toBe('pending')
    expect(store.accessToken).toBeNull()
    expect(store.isAuthenticated).toBe(false)
  })
})

describe('auth store — login', () => {
  it('calls authApi.login with rememberMe param and persists the token pair', async () => {
    mockAuthApi.login.mockResolvedValueOnce(fakeTokenPair)
    mockTauriStorage.setRefreshToken.mockResolvedValueOnce(undefined)
    const store = useAuthStore()

    await store.login('alice', 'pw', true)

    expect(mockAuthApi.login).toHaveBeenCalledWith('alice', 'pw', true)
    expect(mockTauriStorage.setRefreshToken).toHaveBeenCalledWith('refresh.xxx.yyy')
    expect(store.accessToken).toBe('access.aaa.bbb')
    expect(store.user).toEqual(fakeUser)
    expect(store.sessionId).toBe('sid-1')
    expect(store.startupState).toBe('valid')
  })

  it('does NOT write the access token to localStorage', async () => {
    mockAuthApi.login.mockResolvedValueOnce(fakeTokenPair)
    mockTauriStorage.setRefreshToken.mockResolvedValueOnce(undefined)
    const store = useAuthStore()

    await store.login('alice', 'pw')

    const allKeys = Array.from(_store.keys())
    expect(allKeys.some((k) => k.includes('access_token'))).toBe(false)
  })

  it('sets an error message on login failure', async () => {
    mockAuthApi.login.mockRejectedValueOnce(new Error('Invalid credentials'))
    const store = useAuthStore()
    await expect(store.login('alice', 'pw')).rejects.toThrow()
    expect(store.error).toBe('Invalid credentials')
    expect(store.startupState).not.toBe('valid')
  })
})

describe('auth store — startupCheck', () => {
  it('transitions to "invalid" when no refresh token is stored', async () => {
    mockTauriStorage.getRefreshToken.mockResolvedValueOnce(null)
    const store = useAuthStore()

    const state = await store.startupCheck()

    expect(state).toBe('invalid')
    expect(store.startupState).toBe('invalid')
    expect(store.isAuthenticated).toBe(false)
  })

  it('transitions to "valid" when whoami succeeds', async () => {
    mockTauriStorage.getRefreshToken.mockResolvedValueOnce('refresh-token')
    mockAuthApi.whoami.mockResolvedValueOnce(fakeWhoamiResponse)
    const store = useAuthStore()

    const state = await store.startupCheck()

    expect(state).toBe('valid')
    expect(store.accessToken).toBe('access.aaa.bbb')
    expect(store.user).toEqual(fakeUser)
    expect(store.isAuthenticated).toBe(true)
  })

  it('transitions to "invalid" and clears the refresh token on 401', async () => {
    mockTauriStorage.getRefreshToken.mockResolvedValueOnce('refresh-token')
    mockTauriStorage.clearRefreshToken.mockResolvedValueOnce(undefined)
    const authError: Error & { status?: number } = new Error('Unauthorized')
    authError.status = 401
    mockAuthApi.whoami.mockRejectedValueOnce(authError)
    const store = useAuthStore()

    const state = await store.startupCheck()

    expect(state).toBe('invalid')
    expect(mockTauriStorage.clearRefreshToken).toHaveBeenCalled()
    expect(store.startupState).toBe('invalid')
  })

  it('transitions to "error" on network failure and keeps the refresh token', async () => {
    mockTauriStorage.getRefreshToken.mockResolvedValueOnce('refresh-token')
    mockAuthApi.whoami.mockRejectedValueOnce(new Error('Network unreachable'))
    const store = useAuthStore()

    const state = await store.startupCheck()

    expect(state).toBe('error')
    expect(store.startupState).toBe('error')
    // Refresh token retained so the user can retry.
    expect(mockTauriStorage.clearRefreshToken).not.toHaveBeenCalled()
    expect(store.error).toBe('Network unreachable')
  })
})

describe('auth store — silentRefresh', () => {
  it('returns the new pair on success', async () => {
    mockTauriStorage.getRefreshToken.mockResolvedValueOnce('refresh-token')
    mockAuthApi.refresh.mockResolvedValueOnce(fakeTokenPair)
    mockTauriStorage.setRefreshToken.mockResolvedValueOnce(undefined)
    const store = useAuthStore()

    const result = await store.silentRefresh()

    expect(result).toEqual(fakeTokenPair)
    expect(store.accessToken).toBe('access.aaa.bbb')
    expect(store.startupState).toBe('valid')
  })

  it('clears the refresh token on 401 and transitions to invalid', async () => {
    mockTauriStorage.getRefreshToken.mockResolvedValueOnce('refresh-token')
    mockTauriStorage.clearRefreshToken.mockResolvedValueOnce(undefined)
    const authError: Error & { status?: number } = new Error('token_reuse_detected')
    authError.status = 401
    mockAuthApi.refresh.mockRejectedValueOnce(authError)
    const store = useAuthStore()

    const result = await store.silentRefresh()

    expect(result).toBeNull()
    expect(mockTauriStorage.clearRefreshToken).toHaveBeenCalled()
    expect(store.startupState).toBe('invalid')
  })

  it('deduplicates concurrent refresh calls', async () => {
    let resolveRefresh: (v: typeof fakeTokenPair) => void = () => undefined
    mockTauriStorage.getRefreshToken.mockResolvedValue('refresh-token')
    mockAuthApi.refresh.mockReturnValueOnce(
      new Promise((resolve) => {
        resolveRefresh = resolve
      }),
    )
    mockTauriStorage.setRefreshToken.mockResolvedValue(undefined)
    const store = useAuthStore()

    // Fire two concurrent refreshes BEFORE the first one resolves.
    const p1 = store.silentRefresh()
    const p2 = store.silentRefresh()

    // Resolve the underlying mock with the fake pair.
    resolveRefresh(fakeTokenPair)

    const [r1, r2] = await Promise.all([p1, p2])
    expect(r1).toEqual(fakeTokenPair)
    expect(r2).toEqual(fakeTokenPair)
    // The underlying API must only have been called ONCE.
    expect(mockAuthApi.refresh).toHaveBeenCalledTimes(1)
  })
})

describe('auth store — shouldRefresh (pre-emptive refresh trigger)', () => {
  it('returns false when no access token', () => {
    const store = useAuthStore()
    expect(store.shouldRefresh).toBe(false)
  })

  it('returns false when access token is far from expiry', async () => {
    // Build a JWT with exp = now + 10 min.
    const exp = Math.floor(Date.now() / 1000) + 600
    const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
    const payload = btoa(JSON.stringify({ exp }))
    const token = `${header}.${payload}.signature`
    mockAuthApi.login.mockResolvedValueOnce({
      ...fakeTokenPair,
      access_token: token,
    })
    mockTauriStorage.setRefreshToken.mockResolvedValueOnce(undefined)
    const store = useAuthStore()
    await store.login('alice', 'pw')

    expect(store.shouldRefresh).toBe(false)
  })

  it('returns true when access token is within 2 minutes of expiry', async () => {
    const exp = Math.floor(Date.now() / 1000) + 60
    const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
    const payload = btoa(JSON.stringify({ exp }))
    const token = `${header}.${payload}.signature`
    mockAuthApi.login.mockResolvedValueOnce({
      ...fakeTokenPair,
      access_token: token,
    })
    mockTauriStorage.setRefreshToken.mockResolvedValueOnce(undefined)
    const store = useAuthStore()
    await store.login('alice', 'pw')

    expect(store.shouldRefresh).toBe(true)
  })
})

describe('auth store — logout', () => {
  it('calls authApi.logout, clears storage, and transitions to invalid', async () => {
    mockTauriStorage.getRefreshToken.mockResolvedValueOnce('refresh-token')
    mockAuthApi.logout.mockResolvedValueOnce({ revoked: true })
    mockTauriStorage.clearRefreshToken.mockResolvedValueOnce(undefined)
    const store = useAuthStore()
    store.user = fakeUser

    await store.logout()

    expect(mockAuthApi.logout).toHaveBeenCalledWith('refresh-token')
    expect(mockTauriStorage.clearRefreshToken).toHaveBeenCalled()
    expect(store.accessToken).toBeNull()
    expect(store.user).toBeNull()
    expect(store.startupState).toBe('invalid')
  })

  it('clears local state even if the server is unreachable', async () => {
    mockTauriStorage.getRefreshToken.mockResolvedValueOnce('refresh-token')
    mockAuthApi.logout.mockRejectedValueOnce(new Error('Network unreachable'))
    mockTauriStorage.clearRefreshToken.mockResolvedValueOnce(undefined)
    const store = useAuthStore()
    store.user = fakeUser

    await store.logout()

    expect(mockTauriStorage.clearRefreshToken).toHaveBeenCalled()
    expect(store.accessToken).toBeNull()
    expect(store.user).toBeNull()
  })
})

describe('auth store — session management', () => {
  it('listSessions delegates to authApi.listSessions', async () => {
    const sessions = [fakeWhoamiResponse.session]
    mockAuthApi.listSessions.mockResolvedValueOnce(sessions)
    const store = useAuthStore()

    const result = await store.listSessions()

    expect(result).toBe(sessions)
    expect(mockAuthApi.listSessions).toHaveBeenCalled()
  })

  it('revokeSession delegates to authApi.revokeSession', async () => {
    mockAuthApi.revokeSession.mockResolvedValueOnce({ revoked: true })
    const store = useAuthStore()

    await store.revokeSession('sid-1')

    expect(mockAuthApi.revokeSession).toHaveBeenCalledWith('sid-1')
  })

  it('changePassword delegates to authApi.changePassword', async () => {
    mockAuthApi.changePassword.mockResolvedValueOnce({
      changed: true,
      revoked_other_sessions: 0,
    })
    const store = useAuthStore()

    await store.changePassword('old', 'new')

    expect(mockAuthApi.changePassword).toHaveBeenCalledWith('old', 'new')
  })
})

describe('auth store — permission helpers', () => {
  it('admin role can do everything', () => {
    const store = useAuthStore()
    store.user = fakeUser // role = admin
    expect(store.isAdmin()).toBe(true)
    expect(store.canUseLocalTerminal()).toBe(true)
    expect(store.canUseServerTerminal()).toBe(true)
    expect(store.hasPermission('USER_MANAGE')).toBe(true)
    expect(store.hasPermission('SYSTEM_CONFIG')).toBe(true)
  })

  it('member role has restricted permissions', () => {
    const store = useAuthStore()
    store.user = { ...fakeUser, role: 'member' }
    expect(store.isAdmin()).toBe(false)
    expect(store.hasPermission('CHAT')).toBe(true)
    expect(store.hasPermission('KB_QUERY')).toBe(true)
    expect(store.hasPermission('USER_MANAGE')).toBe(false)
  })
})
