import assert from 'node:assert/strict'
import test from 'node:test'
import { MacSpotifyProvider } from '../src/main/services/macSpotifyProvider'
import type {
  NativeWindowsMediaAPI,
  NativeWindowsSpotifyPlaybackState,
} from '../src/types/nativeWindowsMedia'

const DELIMITER = '\u001f'

function createStatusPayload(options: {
  album?: string
  artist?: string
  artworkUrl?: string
  duration?: number
  id?: string
  isFavorite?: boolean
  playbackState: 'playing' | 'paused' | 'stopped'
  position?: number
  title?: string
}): string {
  return [
    'ok',
    options.playbackState,
    String(options.position ?? 0),
    String(options.duration ?? 0),
    options.id ?? '',
    options.title ?? '',
    options.artist ?? '',
    options.album ?? '',
    options.artworkUrl ?? '',
    String(options.isFavorite ?? false),
  ].join(DELIMITER)
}

function createLinuxListNamesPayload(names: string[]): string {
  return `([${names.map((name) => `'${name}'`).join(', ')}],)`
}

function createLinuxStatusPayload(options: {
  album?: string
  artist?: string
  artworkUrl?: string
  durationUs?: number
  playbackState: 'Playing' | 'Paused' | 'Stopped'
  positionUs?: number
  title?: string
  trackId?: string
  trackUrl?: string
}): string {
  const artists = options.artist ? `['${options.artist}']` : '[]'
  return `({'PlaybackStatus': <'${options.playbackState}'>, 'Metadata': <{'mpris:trackid': <objectpath '${options.trackId ?? '/com/spotify/track/123'}'>, 'mpris:length': <int64 ${options.durationUs ?? 0}>, 'mpris:artUrl': <'${options.artworkUrl ?? ''}'>, 'xesam:album': <'${options.album ?? ''}'>, 'xesam:artist': <${artists}>, 'xesam:title': <'${options.title ?? ''}'>, 'xesam:url': <'${options.trackUrl ?? ''}'>}>, 'Position': <int64 ${options.positionUs ?? 0}>},)`
}

async function waitFor(predicate: () => boolean, message: string): Promise<void> {
  const deadline = Date.now() + 2000
  while (Date.now() < deadline) {
    if (predicate()) {
      return
    }
    await new Promise((resolve) => setTimeout(resolve, 0))
  }

  assert.fail(message)
}

class StubSpotifyRunner {
  commandCalls: string[] = []
  commandError: Error | null = null
  statusCalls = 0
  statusError: Error | null = null

  constructor(private readonly statusResponses: string[]) {}

  async run(scriptLines: string[]): Promise<string> {
    const script = scriptLines.join('\n')
    if (script.includes('next track')) {
      return this.handleCommand('next')
    }
    if (script.includes('previous track')) {
      return this.handleCommand('previous')
    }
    if (script.includes('\n  pause\n')) {
      return this.handleCommand('pause')
    }
    if (script.includes('\n  play\n')) {
      return this.handleCommand('play')
    }

    this.statusCalls += 1
    if (this.statusError) {
      throw this.statusError
    }

    return this.statusResponses[Math.min(this.statusCalls - 1, this.statusResponses.length - 1)] ?? 'not_running'
  }

  private handleCommand(command: string): string {
    this.commandCalls.push(command)
    if (this.commandError) {
      throw this.commandError
    }
    return ''
  }
}

class StubCommandRunner {
  calls: Array<{ command: string; args: string[] }> = []
  error: Error | null = null

  constructor(
    private readonly handler: (command: string, args: string[]) => string,
  ) {}

  async run(command: string, args: string[]): Promise<string> {
    this.calls.push({ command, args: [...args] })
    if (this.error) {
      throw this.error
    }
    return this.handler(command, args)
  }
}

class StubWindowsMediaApi implements NativeWindowsMediaAPI {
  calls: Array<{ kind: 'status' | 'control'; command?: string }> = []
  controlError: Error | null = null
  statusError: Error | null = null
  support = {
    available: true,
    reason: null,
  } as const

  constructor(
    private readonly playbackState: NativeWindowsSpotifyPlaybackState | null,
  ) {}

  getSupport() {
    return this.support
  }

  getSpotifyPlaybackState(): NativeWindowsSpotifyPlaybackState | null {
    this.calls.push({ kind: 'status' })
    if (this.statusError) {
      throw this.statusError
    }

    return this.playbackState
  }

  sendSpotifyControl(command: 'play' | 'pause' | 'next' | 'previous'): boolean {
    this.calls.push({ kind: 'control', command })
    if (this.controlError) {
      throw this.controlError
    }

    return true
  }
}

test('provider stays unavailable on unsupported platforms', async () => {
  const provider = new MacSpotifyProvider({
    platform: 'freebsd',
  })

  try {
    await provider.initialize()
    assert.equal(provider.getProviderState().available, false)
    assert.equal(provider.getProviderState().connectionState, 'unavailable')
  } finally {
    await provider.dispose()
  }
})

test('provider reads local macOS Spotify playback and hydrates artwork while active', async () => {
  const runner = new StubSpotifyRunner([
    createStatusPayload({
      playbackState: 'playing',
      position: 42,
      duration: 180000,
      id: 'spotify:track:123',
      title: 'Song One',
      artist: 'Artist One',
      album: 'Album One',
      artworkUrl: 'https://i.scdn.co/image/cover-one',
      isFavorite: true,
    }),
  ])

  const provider = new MacSpotifyProvider({
    accessImpl: async () => undefined,
    fetchImpl: async () => new Response(Buffer.from('cover-one'), {
      status: 200,
      headers: {
        'content-type': 'image/png',
      },
    }),
    now: () => 1000,
    platform: 'darwin',
    runner: (scriptLines) => runner.run(scriptLines),
  })

  try {
    await provider.initialize()
    await provider.setConsumerActive(1, true)
    await waitFor(() => provider.getProviderState().connectionState === 'connected', 'expected connected Spotify state')

    const state = provider.getProviderState()
    assert.equal(state.snapshot?.currentTrack?.title, 'Song One')
    assert.equal(state.snapshot?.currentTrack?.isFavorite, true)
    assert.match(state.snapshot?.currentTrack?.artworkDataUrl ?? '', /^data:image\/png;base64,/)
    assert.equal(state.snapshot?.playbackState, 'playing')
    assert.equal(state.snapshot?.currentTime, 42)
    assert.equal(state.snapshot?.duration, 180)
  } finally {
    await provider.dispose()
  }
})

test('provider treats a non-running macOS Spotify app as idle instead of erroring', async () => {
  const provider = new MacSpotifyProvider({
    accessImpl: async () => undefined,
    platform: 'darwin',
    runner: async () => 'not_running',
  })

  try {
    await provider.initialize()
    await provider.setConsumerActive(1, true)
    await waitFor(() => provider.getProviderState().connectionState === 'disabled', 'expected disabled Spotify state')
    assert.equal(provider.getProviderState().lastError, null)
  } finally {
    await provider.dispose()
  }
})

test('provider surfaces macOS Automation permission failures during polling', async () => {
  const provider = new MacSpotifyProvider({
    accessImpl: async () => undefined,
    platform: 'darwin',
    runner: async () => {
      throw new Error('Not authorized to send Apple events to Spotify. (-1743)')
    },
  })

  try {
    await provider.initialize()
    await provider.setConsumerActive(1, true)
    await waitFor(() => provider.getProviderState().connectionState === 'error', 'expected Spotify error state')
    assert.match(provider.getProviderState().lastError ?? '', /Automation permission/)
  } finally {
    await provider.dispose()
  }
})

test('provider routes macOS transport controls through AppleScript and records control failures', async () => {
  const runner = new StubSpotifyRunner([
    createStatusPayload({
      playbackState: 'paused',
      position: 0,
      duration: 180000,
      id: 'spotify:track:123',
      title: 'Song One',
      artist: 'Artist One',
      album: 'Album One',
    }),
  ])
  const provider = new MacSpotifyProvider({
    accessImpl: async () => undefined,
    platform: 'darwin',
    runner: (scriptLines) => runner.run(scriptLines),
  })

  try {
    await provider.initialize()
    await provider.sendControl('next')
    assert.deepEqual(runner.commandCalls, ['next'])

    runner.commandError = new Error('Not authorized to send Apple events to Spotify. (-1743)')
    await assert.rejects(() => provider.sendControl('play'), /Automation permission/)
    assert.match(provider.getProviderState().lastControlError ?? '', /Automation permission/)
  } finally {
    await provider.dispose()
  }
})

test('provider reads Linux Spotify playback through MPRIS and routes controls through gdbus', async () => {
  const runner = new StubCommandRunner((command, args) => {
    assert.equal(command, 'gdbus')
    const methodIndex = args.indexOf('--method')
    const method = methodIndex >= 0 ? args[methodIndex + 1] : ''

    switch (method) {
      case 'org.freedesktop.DBus.ListNames':
        return createLinuxListNamesPayload([
          'org.freedesktop.DBus',
          'org.mpris.MediaPlayer2.spotify',
        ])
      case 'org.freedesktop.DBus.Properties.GetAll':
        return createLinuxStatusPayload({
          playbackState: 'Playing',
          positionUs: 42000000,
          durationUs: 180000000,
          title: 'Song Linux',
          artist: 'Artist Linux',
          album: 'Album Linux',
          artworkUrl: 'https://i.scdn.co/image/linux-cover',
          trackUrl: 'spotify:track:linux123',
        })
      case 'org.mpris.MediaPlayer2.Player.Next':
        return '()'
      default:
        throw new Error(`Unhandled Linux method ${method}`)
    }
  })

  const provider = new MacSpotifyProvider({
    commandRunner: (command, args) => runner.run(command, args),
    fetchImpl: async () => new Response(Buffer.from('linux-cover'), {
      status: 200,
      headers: {
        'content-type': 'image/png',
      },
    }),
    now: () => 2000,
    platform: 'linux',
  })

  try {
    await provider.initialize()
    assert.equal(provider.getProviderState().available, true)
    await provider.setConsumerActive(1, true)
    await waitFor(() => provider.getProviderState().connectionState === 'connected', 'expected connected Linux Spotify state')

    const state = provider.getProviderState()
    assert.equal(state.snapshot?.currentTrack?.title, 'Song Linux')
    assert.equal(state.snapshot?.currentTrack?.artist, 'Artist Linux')
    assert.equal(state.snapshot?.currentTrack?.isFavorite, false)
    assert.match(state.snapshot?.currentTrack?.artworkDataUrl ?? '', /^data:image\/png;base64,/)
    assert.equal(state.snapshot?.currentTime, 42)
    assert.equal(state.snapshot?.duration, 180)

    await provider.sendControl('next')
    assert.ok(runner.calls.some((call) => call.args.includes('org.mpris.MediaPlayer2.Player.Next')))
  } finally {
    await provider.dispose()
  }
})

test('provider treats a missing Linux Spotify MPRIS session as idle', async () => {
  const provider = new MacSpotifyProvider({
    commandRunner: async (_command, args) => {
      if (args.includes('org.freedesktop.DBus.ListNames')) {
        return createLinuxListNamesPayload(['org.freedesktop.DBus'])
      }

      throw new Error('unexpected command')
    },
    platform: 'linux',
  })

  try {
    await provider.initialize()
    assert.equal(provider.getProviderState().available, true)
    await provider.setConsumerActive(1, true)
    await waitFor(() => provider.getProviderState().connectionState === 'disabled', 'expected disabled Linux Spotify state')
    assert.equal(provider.getProviderState().lastError, null)
  } finally {
    await provider.dispose()
  }
})

test('provider stays unavailable on Windows when the native media API is not present', async () => {
  const provider = new MacSpotifyProvider({
    platform: 'win32',
  })

  try {
    await provider.initialize()
    assert.equal(provider.getProviderState().available, false)
    assert.match(provider.getProviderState().lastError ?? '', /native Windows media integration is not available/)
  } finally {
    await provider.dispose()
  }
})

test('provider reads Windows Spotify playback through system media controls and routes commands', async () => {
  const windowsMediaApi = new StubWindowsMediaApi({
    playbackStatus: 'Playing',
    positionMs: 32000,
    durationMs: 210000,
    title: 'Song Windows',
    artist: 'Artist Windows',
    album: 'Album Windows',
    sourceAppUserModelId: 'SpotifyAB.SpotifyMusic_zpdnekdrzrea0!Spotify',
  })

  const provider = new MacSpotifyProvider({
    now: () => 3000,
    platform: 'win32',
    windowsMediaApi,
  })

  try {
    await provider.initialize()
    assert.equal(provider.getProviderState().available, true)
    await provider.setConsumerActive(1, true)
    await waitFor(() => provider.getProviderState().connectionState === 'connected', 'expected connected Windows Spotify state')

    const state = provider.getProviderState()
    assert.equal(state.snapshot?.currentTrack?.title, 'Song Windows')
    assert.equal(state.snapshot?.currentTrack?.artist, 'Artist Windows')
    assert.equal(state.snapshot?.currentTrack?.isFavorite, false)
    assert.equal(state.snapshot?.currentTime, 32)
    assert.equal(state.snapshot?.duration, 210)

    await provider.sendControl('next')
    assert.ok(windowsMediaApi.calls.some((call) => call.kind === 'control' && call.command === 'next'))
  } finally {
    await provider.dispose()
  }
})
