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

// Mock api/client.js — no real HTTP calls
vi.mock('../../api/client.js', () => ({
  login: vi.fn(),
  register: vi.fn(),
  logout: vi.fn(),
  logoutAll: vi.fn(),
  refreshToken: vi.fn(),
  getMyQuota: vi.fn(),
}))

import { useAuthStore } from '../auth.js'
import * as api from '../../api/client.js'

// ── Fake localStorage / sessionStorage to detect any writes ──────────────────
// happy-dom may not provide window.localStorage, so we install our own stubs
// and check whether they were called.

const fakeLocalStorage = {
  _store: {},
  setItem: vi.fn(),
  getItem: vi.fn(key => null),
  removeItem: vi.fn(),
  clear: vi.fn(),
}

const fakeSessionStorage = {
  _store: {},
  setItem: vi.fn(),
  getItem: vi.fn(key => null),
  removeItem: vi.fn(),
  clear: vi.fn(),
}

// Install stubs globally before tests
Object.defineProperty(globalThis, 'localStorage', {
  value: fakeLocalStorage,
  writable: true,
  configurable: true,
})

Object.defineProperty(globalThis, 'sessionStorage', {
  value: fakeSessionStorage,
  writable: true,
  configurable: true,
})

beforeEach(() => {
  setActivePinia(createPinia())
  vi.clearAllMocks()
  // Reset the storage spies
  fakeLocalStorage.setItem.mockClear()
  fakeSessionStorage.setItem.mockClear()
})

afterEach(() => {
  // nothing to restore — vi.clearAllMocks() handles it
})

// ── Security invariant: no browser storage writes ─────────────────────────────

describe('useAuthStore — no browser storage writes (security invariant)', () => {
  it('login() never writes accessToken to localStorage', async () => {
    api.login.mockResolvedValue({
      access_token: 'test-access-token',
      user: { id: '1', handle: 'alice', email: 'alice@example.com', role: 'user', totp_enabled: false },
    })

    const store = useAuthStore()
    await store.login('alice@example.com', 'password')

    // accessToken must be in memory, NOT localStorage
    expect(fakeLocalStorage.setItem).not.toHaveBeenCalled()
  })

  it('login() never writes accessToken to sessionStorage', async () => {
    api.login.mockResolvedValue({
      access_token: 'test-access-token',
      user: { id: '1', handle: 'alice', email: 'alice@example.com', role: 'user', totp_enabled: false },
    })

    const store = useAuthStore()
    await store.login('alice@example.com', 'password')

    expect(fakeSessionStorage.setItem).not.toHaveBeenCalled()
  })

  it('login() stores accessToken in memory ref (not null)', async () => {
    api.login.mockResolvedValue({
      access_token: 'eyJhbGciOiJIUzI1NiJ9.test',
      user: { id: '1', handle: 'alice', email: 'alice@example.com', role: 'user', totp_enabled: false },
    })

    const store = useAuthStore()
    await store.login('alice@example.com', 'password')

    expect(store.accessToken).toBe('eyJhbGciOiJIUzI1NiJ9.test')
  })

  it('logout() clears accessToken from memory without writing to any storage', async () => {
    api.login.mockResolvedValue({
      access_token: 'some-token',
      user: { id: '1', handle: 'bob', email: 'bob@example.com', role: 'user', totp_enabled: false },
    })
    api.logout.mockResolvedValue(null)

    const store = useAuthStore()
    await store.login('bob@example.com', 'pass')
    expect(store.accessToken).toBe('some-token')

    await store.logout()

    expect(store.accessToken).toBeNull()
    // No storage writes during logout either
    expect(fakeLocalStorage.setItem).not.toHaveBeenCalledWith(
      expect.stringMatching(/token|auth|access/i),
      expect.anything()
    )
  })
})

// ── login() passes totp_code to API ──────────────────────────────────────────

describe('useAuthStore — login() forwards TOTP/backup codes', () => {
  it('login() with options.totpCode sends totp_code in API request body', async () => {
    api.login.mockResolvedValue({
      access_token: 'tok',
      user: { id: '1', handle: 'u', email: 'u@x.com', role: 'user', totp_enabled: true },
    })

    const store = useAuthStore()
    await store.login('u@x.com', 'pass', { totpCode: '123456' })

    expect(api.login).toHaveBeenCalledWith(
      expect.objectContaining({ totp_code: '123456' })
    )
  })

  it('login() with options.backupCode sends backup_code in API request body', async () => {
    api.login.mockResolvedValue({
      access_token: 'tok',
      user: { id: '1', handle: 'u', email: 'u@x.com', role: 'user', totp_enabled: true },
    })

    const store = useAuthStore()
    await store.login('u@x.com', 'pass', { backupCode: 'ABC12345' })

    expect(api.login).toHaveBeenCalledWith(
      expect.objectContaining({ backup_code: 'ABC12345' })
    )
  })

  it('login() without options sends null for both totp_code and backup_code', async () => {
    api.login.mockResolvedValue({
      access_token: 'tok',
      user: { id: '1', handle: 'u', email: 'u@x.com', role: 'user', totp_enabled: false },
    })

    const store = useAuthStore()
    await store.login('u@x.com', 'pass')

    expect(api.login).toHaveBeenCalledWith(
      expect.objectContaining({
        totp_code: null,
        backup_code: null,
      })
    )
  })

  it('login() returns { requires_totp: true } when server demands TOTP', async () => {
    api.login.mockResolvedValue({ requires_totp: true })

    const store = useAuthStore()
    const result = await store.login('u@x.com', 'pass')

    expect(result).toEqual({ requires_totp: true })
    // accessToken must remain null — no tokens were issued
    expect(store.accessToken).toBeNull()
  })

  it('login() returns { requires_password_change: true, user_id } when forced change', async () => {
    api.login.mockResolvedValue({ requires_password_change: true, user_id: 'uid-99' })

    const store = useAuthStore()
    const result = await store.login('u@x.com', 'pass')

    expect(result).toEqual({ requires_password_change: true, user_id: 'uid-99' })
    expect(store.accessToken).toBeNull()
  })
})

// ── login() field mapping edge cases ─────────────────────────────────────────

describe('useAuthStore — login() API field mapping', () => {
  it('sends email and password as-is', async () => {
    api.login.mockResolvedValue({
      access_token: 'tok',
      user: { id: '1', handle: 'u', email: 'test@example.com', role: 'user', totp_enabled: false },
    })

    const store = useAuthStore()
    await store.login('test@example.com', 'S3cr3tP@ss!')

    expect(api.login).toHaveBeenCalledWith(
      expect.objectContaining({
        email: 'test@example.com',
        password: 'S3cr3tP@ss!',
      })
    )
  })

  it('sets user in state after successful login', async () => {
    const userData = { id: 'user-1', handle: 'alice', email: 'alice@example.com', role: 'user', totp_enabled: false }
    api.login.mockResolvedValue({ access_token: 'tok', user: userData })

    const store = useAuthStore()
    await store.login('alice@example.com', 'pass')

    expect(store.user).toEqual(userData)
  })
})
