// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'

// The Appearance tab reads `/appearance` (settings + resolved brand), then on
// save read-modify-writes `/globals/settings`. Uploads POST `/media/upload`.
let appearanceData: any

const cmsApi = vi.fn(async (endpoint: string, options?: { method?: string; body?: unknown }) => {
  const method = options?.method ?? 'GET'
  if (endpoint === '/appearance' && method === 'GET') return { data: appearanceData, status: 200 }
  if (endpoint === '/globals/settings' && method === 'GET')
    return { data: { siteTitle: 'Actuate Media' }, status: 200 }
  if (endpoint === '/globals/settings' && method === 'PUT') return { data: {}, status: 200 }
  if (endpoint === '/media/upload' && method === 'POST')
    return {
      data: { id: 'new-logo', url: 'https://cdn.example.com/x.svg', mimeType: 'image/svg+xml' },
      status: 200,
    }
  if (endpoint.startsWith('/media?') && method === 'GET')
    return {
      data: {
        items: [
          {
            id: 'existing-logo',
            filename: 'existing.png',
            storageKey: 'https://cdn.example.com/existing.png',
            url: 'https://cdn.example.com/existing.png',
            mimeType: 'image/png',
            fileSize: 1234,
            width: 400,
            height: 100,
          },
        ],
      },
      status: 200,
    }
  return { data: {}, status: 200 }
})

vi.mock('../../lib/api.js', () => ({ cmsApi: (e: string, o?: unknown) => cmsApi(e, o as any) }))

const { AppearanceTab } = await import('../../views/settings/AppearanceTab.js')
const { LOGO_MIME, FAVICON_MIME } = await import('../../views/settings/useAppearanceSettings.js')

function baseData(overrides: Record<string, unknown> = {}) {
  return {
    appearance: {
      adminColorModeDefault: 'system',
      adminAccentColor: 'purple',
      sidebarDensity: 'default',
      contentFont: 'DM Sans',
      reduceMotion: false,
    },
    brand: { mode: 'inheritPublicBrand' },
    resolved: { admin: { initials: 'AM' } },
    assets: [],
    siteTitle: 'Actuate Media',
    ...overrides,
  }
}

beforeEach(() => {
  cmsApi.mockClear()
  appearanceData = baseData()
})

describe('AppearanceTab', () => {
  it('renders the branding/theme/typography cards with a clean (disabled) save bar', async () => {
    render(<AppearanceTab role="ADMIN" />)

    await screen.findByText('Branding')
    expect(screen.getByText('Admin theme')).toBeTruthy()
    expect(screen.getByText('Typography & motion')).toBeTruthy()
    expect(screen.getByText('Brand preview')).toBeTruthy()
    expect(
      (screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement).disabled,
    ).toBe(true)
  })

  it('enables Save when the accent changes and persists appearance to the settings global', async () => {
    render(<AppearanceTab role="ADMIN" />)
    await screen.findByText('Branding')

    fireEvent.click(screen.getByRole('radio', { name: 'Blue' }))
    const save = screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement
    expect(save.disabled).toBe(false)

    fireEvent.click(save)

    await waitFor(() =>
      expect(cmsApi).toHaveBeenCalledWith(
        '/globals/settings',
        expect.objectContaining({
          method: 'PUT',
          body: expect.stringContaining('"adminAccentColor":"blue"'),
        }),
      ),
    )
  })

  it('warns when dark mode is the default but no dark-mode logo is set', async () => {
    appearanceData = baseData({
      appearance: {
        adminColorModeDefault: 'dark',
        adminAccentColor: 'purple',
        sidebarDensity: 'default',
        contentFont: 'DM Sans',
        reduceMotion: false,
      },
    })
    render(<AppearanceTab role="ADMIN" />)

    expect(await screen.findByText(/no dark-mode logo is set/i)).toBeTruthy()
  })

  it('uploads a logo through the Media Center and enables Save', async () => {
    render(<AppearanceTab role="ADMIN" />)
    await screen.findByText('Branding')

    const input = screen.getByLabelText('Primary logo') as HTMLInputElement
    const file = new File(
      ['<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg"></svg>'],
      'logo.svg',
      { type: 'image/svg+xml' },
    )
    fireEvent.change(input, { target: { files: [file] } })

    await waitFor(() =>
      expect(cmsApi).toHaveBeenCalledWith(
        '/media/upload',
        expect.objectContaining({ method: 'POST' }),
      ),
    )
    await waitFor(() =>
      expect(
        (screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement).disabled,
      ).toBe(false),
    )
  })

  it('references an existing media-library asset without re-uploading', async () => {
    render(<AppearanceTab role="ADMIN" />)
    await screen.findByText('Branding')

    fireEvent.click(screen.getByRole('button', { name: /choose primary logo from media library/i }))

    const thumb = await screen.findByRole('button', { name: /existing\.png/i })
    fireEvent.click(thumb)

    // No upload should be issued — we reference the existing asset by id.
    expect(cmsApi.mock.calls.some(([endpoint]) => endpoint === '/media/upload')).toBe(false)

    const save = await screen.findByRole('button', { name: 'Save Changes' })
    await waitFor(() => expect((save as HTMLButtonElement).disabled).toBe(false))
    fireEvent.click(save)

    await waitFor(() =>
      expect(cmsApi).toHaveBeenCalledWith(
        '/globals/settings',
        expect.objectContaining({
          method: 'PUT',
          body: expect.stringContaining('"publicPrimaryLogoAssetId":"existing-logo"'),
        }),
      ),
    )
  })

  it('accepts modern image formats for logos (and a crisp subset for favicons)', () => {
    // WebP is a first-class brand format — general media uploads are converted
    // to WebP anyway, so re-selecting them from the library must work.
    expect(LOGO_MIME).toEqual(
      expect.arrayContaining([
        'image/svg+xml',
        'image/png',
        'image/webp',
        'image/jpeg',
        'image/gif',
        'image/avif',
      ]),
    )
    expect(FAVICON_MIME).toEqual(
      expect.arrayContaining(['image/svg+xml', 'image/png', 'image/webp']),
    )
    // Favicons need crisp small-size rendering — lossy photographic formats stay out.
    expect(FAVICON_MIME).not.toContain('image/jpeg')
  })

  it('uploads a WebP logo through the Media Center', async () => {
    // happy-dom's Image never decodes blobs — fire onload so shape detection
    // (raster path, used for all non-SVG formats) completes.
    vi.stubGlobal(
      'Image',
      class {
        naturalWidth = 400
        naturalHeight = 100
        onload: (() => void) | null = null
        onerror: (() => void) | null = null
        set src(_v: string) {
          queueMicrotask(() => this.onload?.())
        }
      },
    )

    render(<AppearanceTab role="ADMIN" />)
    await screen.findByText('Branding')

    const input = screen.getByLabelText('Primary logo') as HTMLInputElement
    const file = new File([new Uint8Array([0x52, 0x49, 0x46, 0x46])], 'logo.webp', {
      type: 'image/webp',
    })
    fireEvent.change(input, { target: { files: [file] } })

    await waitFor(() =>
      expect(cmsApi).toHaveBeenCalledWith(
        '/media/upload',
        expect.objectContaining({ method: 'POST' }),
      ),
    )
    vi.unstubAllGlobals()
  })

  it('rejects an unsupported upload type client-side with a readable error', async () => {
    render(<AppearanceTab role="ADMIN" />)
    await screen.findByText('Branding')

    const input = screen.getByLabelText('Primary logo') as HTMLInputElement
    const file = new File(['%PDF-1.4'], 'brochure.pdf', { type: 'application/pdf' })
    fireEvent.change(input, { target: { files: [file] } })

    expect(await screen.findByText(/unsupported file type/i)).toBeTruthy()
    expect(cmsApi.mock.calls.some(([endpoint]) => endpoint === '/media/upload')).toBe(false)
  })

  it('is read-only for non-admins', async () => {
    render(<AppearanceTab role="EDITOR" />)

    expect(await screen.findByText(/read-only access to appearance/i)).toBeTruthy()
    // Upload control disabled.
    expect(
      (screen.getByRole('button', { name: /upload primary logo/i }) as HTMLButtonElement).disabled,
    ).toBe(true)
  })
})
