import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { loadLib } from './helpers/loadLib.js'

const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
const manifest = JSON.parse(readFileSync(path.join(EXT_DIR, 'manifest.json'), 'utf8'))

const { getPlatformFromUrl, isArtistPage, PLATFORMS, PLATFORM_ARTIST_PATTERNS } = loadLib(
  'platforms.js',
  ['getPlatformFromUrl', 'isArtistPage', 'PLATFORMS', 'PLATFORM_ARTIST_PATTERNS']
)

describe('getPlatformFromUrl', () => {
  it('identifies each platform from a domain URL', () => {
    expect(getPlatformFromUrl('https://www.patreon.com/Atole')).toBe('patreon')
    expect(getPlatformFromUrl('https://subscribestar.adult/someone')).toBe('subscribestar')
    expect(getPlatformFromUrl('https://www.hentai-foundry.com/user/someone')).toBe('hentaifoundry')
    expect(getPlatformFromUrl('https://discord.com/channels/@me')).toBe('discord')
    expect(getPlatformFromUrl('https://www.pixiv.net/en/users/123')).toBe('pixiv')
  })

  it('accepts http as well as https, with or without www', () => {
    expect(getPlatformFromUrl('http://patreon.com/Atole')).toBe('patreon')
    expect(getPlatformFromUrl('https://www.patreon.com/Atole')).toBe('patreon')
  })

  it('returns null for unrelated hosts', () => {
    expect(getPlatformFromUrl('https://example.com/patreon.com')).toBe(null)
    expect(getPlatformFromUrl('https://not-patreon.com/Atole')).toBe(null)
    expect(getPlatformFromUrl('')).toBe(null)
  })

  it('returns null for deviantart, retired at #3069', () => {
    // The 2026-07-05 product decision (FC downloaders = art-dedicated services
    // only) left deviantart wired for seven weeks. Asserting the negative is
    // what keeps a partial retirement from being re-completed by accident.
    expect(getPlatformFromUrl('https://www.deviantart.com/someone')).toBe(null)
    expect(PLATFORMS.deviantart).toBeUndefined()
    expect(PLATFORM_ARTIST_PATTERNS.deviantart).toBeUndefined()
  })
})

describe('isArtistPage', () => {
  // Regression cases from issue #1485: the Add-to-FC button vanished once the
  // operator SUBSCRIBED to a creator, because Patreon serves subscribed users
  // the /cw/ ("creator workspace") URL and the pattern only matched the bare
  // root. All three creator URL shapes must match, plus inner pages — the
  // button matters most exactly when you're subscribed.
  it('matches all three Patreon creator URL shapes', () => {
    expect(isArtistPage('https://www.patreon.com/Atole', 'patreon')).toBe(true)
    expect(isArtistPage('https://www.patreon.com/c/Atole', 'patreon')).toBe(true)
    expect(isArtistPage('https://www.patreon.com/cw/Atole', 'patreon')).toBe(true)
  })

  it('matches Patreon creator inner pages', () => {
    expect(isArtistPage('https://www.patreon.com/cw/Atole/posts', 'patreon')).toBe(true)
    expect(isArtistPage('https://www.patreon.com/Atole/membership', 'patreon')).toBe(true)
  })

  it('excludes Patreon navigation pages that are not creators', () => {
    for (const nav of ['home', 'search', 'messages', 'notifications', 'library', 'settings']) {
      expect(isArtistPage(`https://www.patreon.com/${nav}`, 'patreon')).toBe(false)
      expect(isArtistPage(`https://www.patreon.com/${nav}/anything`, 'patreon')).toBe(false)
    }
  })

  it('matches SubscribeStar creator roots on both TLDs but not feed pages', () => {
    expect(isArtistPage('https://subscribestar.adult/someone', 'subscribestar')).toBe(true)
    expect(isArtistPage('https://subscribestar.com/someone', 'subscribestar')).toBe(true)
    expect(isArtistPage('https://subscribestar.adult/feed', 'subscribestar')).toBe(false)
    expect(isArtistPage('https://subscribestar.adult/messages', 'subscribestar')).toBe(false)
  })

  it('matches Hentai Foundry user pages only', () => {
    expect(isArtistPage('https://www.hentai-foundry.com/user/someone', 'hentaifoundry')).toBe(true)
    expect(isArtistPage('https://www.hentai-foundry.com/pictures/popular', 'hentaifoundry')).toBe(
      false
    )
  })

  it('matches Pixiv numeric user pages, with or without the /en/ prefix', () => {
    expect(isArtistPage('https://www.pixiv.net/users/12345', 'pixiv')).toBe(true)
    expect(isArtistPage('https://www.pixiv.net/en/users/12345', 'pixiv')).toBe(true)
    expect(isArtistPage('https://www.pixiv.net/en/artworks/999', 'pixiv')).toBe(false)
  })

  it('returns false for a platform with no artist pattern (discord)', () => {
    expect(isArtistPage('https://discord.com/channels/@me', 'discord')).toBe(false)
  })

  it('returns false for an unknown platform key', () => {
    expect(isArtistPage('https://www.patreon.com/Atole', 'nope')).toBe(false)
  })
})

describe('platform table integrity', () => {
  it('gives every artist pattern a corresponding platform entry', () => {
    // A pattern keyed to a platform that no longer exists is dead code that
    // silently never fires; the reverse (a platform with no pattern) is the
    // legitimate discord case, so only this direction is an error.
    for (const key of Object.keys(PLATFORM_ARTIST_PATTERNS)) {
      expect(Object.keys(PLATFORMS)).toContain(key)
    }
  })

  it('gives every platform the fields the popup renders', () => {
    for (const [key, platform] of Object.entries(PLATFORMS)) {
      expect(platform.name, `${key}.name`).toBeTruthy()
      expect(platform.color, `${key}.color`).toMatch(/^#[0-9A-Fa-f]{6}$/)
      expect(['cookies', 'token'], `${key}.authType`).toContain(platform.authType)
      expect(platform.urlPattern, `${key}.urlPattern`).toBeInstanceOf(RegExp)
      expect(Array.isArray(platform.domains), `${key}.domains`).toBe(true)
      expect(platform.domains.length, `${key}.domains`).toBeGreaterThan(0)
    }
  })

  it('keeps every artist URL matched by its own platform pattern too', () => {
    // isArtistPage is only ever consulted after getPlatformFromUrl resolves a
    // key, so an artist pattern matching a URL its platform's urlPattern
    // rejects would be unreachable.
    const samples = {
      patreon: 'https://www.patreon.com/cw/Atole',
      subscribestar: 'https://subscribestar.adult/someone',
      hentaifoundry: 'https://www.hentai-foundry.com/user/someone',
      pixiv: 'https://www.pixiv.net/en/users/12345'
    }
    for (const [key, url] of Object.entries(samples)) {
      expect(isArtistPage(url, key), `${key} artist pattern`).toBe(true)
      expect(getPlatformFromUrl(url), `${key} urlPattern`).toBe(key)
    }
  })
})

describe('manifest.json agrees with the platform table', () => {
  // #3069: deviantart was dropped from the product in July but survived in
  // manifest.json until late August, because NOTHING tied the manifest's
  // domain lists back to PLATFORMS. These two specs are that tie. Both
  // directions matter: a stale match ships host access the product decided
  // not to use, and a missing one silently kills the Add-to-FC button.
  const matches = manifest.content_scripts[0].matches
  // '*://*.patreon.com/*' -> '.patreon.com', the form PLATFORMS.domains uses.
  const hostOf = (m) => m.replace(/^\*:\/\/\*/, '').replace(/\/\*$/, '')

  it('injects the content script only on domains a platform claims', () => {
    for (const m of matches) {
      const host = hostOf(m)
      const owner = Object.entries(PLATFORMS).find(
        ([, p]) => p.domains.includes(host)
      )
      expect(owner, `no platform claims content-script match "${m}"`).toBeTruthy()
      // The content script exists to draw the Add-as-source button, so a
      // platform with no artist pattern (discord) has no business here.
      expect(
        PLATFORM_ARTIST_PATTERNS[owner[0]],
        `"${m}" injects for ${owner[0]}, which has no artist pattern`
      ).toBeTruthy()
    }
  })

  it('injects on every platform that has an artist pattern', () => {
    const covered = new Set(
      matches
        .map(hostOf)
        .map((h) => Object.entries(PLATFORMS).find(([, p]) => p.domains.includes(h)))
        .filter(Boolean)
        .map(([key]) => key)
    )
    for (const key of Object.keys(PLATFORM_ARTIST_PATTERNS)) {
      expect(covered, `${key} has an artist pattern but no content-script match`).toContain(key)
    }
  })

  it('requests no host permission for a domain no platform claims', () => {
    // '*://*/*' is the deliberate exception: FC is self-hosted at an arbitrary
    // operator-chosen URL, so the extension cannot enumerate its own backend.
    // Every OTHER entry is a platform domain and must still have an owner.
    for (const h of manifest.host_permissions) {
      if (h === '*://*/*') continue
      const host = hostOf(h)
      // pixiv's OAuth/API hosts are pixiv infrastructure, not creator pages,
      // so they are matched by suffix rather than by the domains list.
      const claimed = Object.values(PLATFORMS).some(
        (p) => p.domains.includes(host) || p.domains.some((d) => host.endsWith(d))
      )
      expect(claimed, `host permission "${h}" belongs to no platform`).toBe(true)
    }
  })
})
