import { sql, eq } from 'drizzle-orm'
import { createPgliteClient } from '@platform-modules/db/pglite'
import { hashToken } from '@platform-modules/util/tokens'
import {
  authorize as issueAuthorizationCode,
  deriveCodeChallengeS256,
  getClient,
  registerClient,
} from '@platform-modules/auth/oauth-provider'
import { beforeEach, describe, expect, it } from 'vitest'

import { onError } from '../http.js'
import { accounts, credentials, oauthTokens, plugins, pressZoneInitSql, pressZoneSchema, sites } from '../schema.js'
import { createOauthRoute } from './oauth.js'

async function createDb() {
  const db = createPgliteClient({ schema: pressZoneSchema })

  for (const statement of pressZoneInitSql.split(';').map((part) => part.trim()).filter(Boolean)) {
    await db.execute(sql.raw(statement))
  }

  return db
}

async function readJson(response: Response): Promise<unknown> {
  return response.json()
}

async function seedAccountAndPlugin(db: Awaited<ReturnType<typeof createDb>>) {
  const accountId = '11111111-1111-1111-1111-111111111111'

  await db.insert(accounts).values({
    id: accountId,
    slug: 'acct-press-zone',
  })
  await db.insert(plugins).values({
    key: 'translate',
    name: 'Translate',
  })

  return { accountId }
}

async function registerTranslateClient(db: Awaited<ReturnType<typeof createDb>>) {
  await registerClient(db, {
    id: 'translate',
    name: 'Translate',
    redirectUris: ['https://plugin.example.com/oauth/callback'],
    scopes: ['plugin:translate'],
    confidential: false,
  })
  const client = await getClient(db, 'translate')
  if (!client) {
    throw new Error('registered oauth client not found')
  }
  return client
}

async function getStoredOauthToken(
  db: Awaited<ReturnType<typeof createDb>>,
  accessToken: string,
) {
  const secret = accessToken.split('.')[1]
  if (!secret) {
    return null
  }

  const accessTokenHash = await hashToken(secret)
  const [token] = await db
    .select()
    .from(oauthTokens)
    .where(eq(oauthTokens.accessTokenHash, accessTokenHash))
    .limit(1)

  return token ?? null
}

function createRequest(body: URLSearchParams): Request {
  return new Request('http://press-zone.test/oauth/token', {
    method: 'POST',
    headers: {
      'content-type': 'application/x-www-form-urlencoded',
    },
    body: body.toString(),
  })
}

describe('oauthRoute', () => {
  beforeEach(() => {
    // isolate any future global spies/mocks
  })

  it('returns an authorization code envelope from /oauth/authorize', async () => {
    const db = await createDb()
    const client = await registerTranslateClient(db)
    const verifier = 'pkce-verifier-authorize-route'
    const challenge = await deriveCodeChallengeS256(verifier)
    const app = createOauthRoute({ db })

    const response = await app.request(
      `http://press-zone.test/oauth/authorize?client_id=${client.id}&redirect_uri=${encodeURIComponent(
        client.redirectUris[0]!,
      )}&scope=plugin%3Atranslate&state=connect-state&code_challenge=${challenge}&code_challenge_method=S256`,
    )

    expect(response.status).toBe(200)
    await expect(readJson(response)).resolves.toEqual({
      data: {
        code: expect.any(String),
        state: 'connect-state',
      },
    })
  })

  it('exchanges a code, returns a bearer token, and stores a site credential link', async () => {
    const db = await createDb()
    const { accountId } = await seedAccountAndPlugin(db)
    const client = await registerTranslateClient(db)
    const verifier = 'pkce-verifier-token-route'
    const challenge = await deriveCodeChallengeS256(verifier)
    const authorized = await issueAuthorizationCode(db, {
      clientId: client.id,
      redirectUri: client.redirectUris[0]!,
      codeChallenge: challenge,
      codeChallengeMethod: 'S256',
      scope: 'plugin:translate',
    })
    const app = createOauthRoute({ db })

    const response = await app.request(
      createRequest(
        new URLSearchParams({
          grant_type: 'authorization_code',
          code: authorized.code,
          code_verifier: verifier,
          client_id: client.id,
          account_id: accountId,
        }),
      ),
    )

    expect(response.status).toBe(200)
    const payload = (await readJson(response)) as {
      data: {
        accessToken: string
        tokenType: string
        expiresIn: number
        scope: string
      }
    }

    expect(payload).toMatchObject({
      data: {
        accessToken: expect.any(String),
        tokenType: 'Bearer',
        expiresIn: 3600,
        scope: 'plugin:translate',
      },
    })

    const [site] = await db.select().from(sites).limit(1)
    expect(site).toMatchObject({
      accountId,
      pluginKey: 'translate',
      displayUrl: 'https://plugin.example.com/oauth/callback',
      status: 'active',
    })

    const storedToken = await getStoredOauthToken(db, payload.data.accessToken)
    expect(storedToken).toMatchObject({
      clientId: client.id,
      tokenType: 'Bearer',
      scope: 'plugin:translate',
    })

    const [credential] = await db.select().from(credentials).limit(1)
    expect(credential).toMatchObject({
      accountId,
      siteId: site?.id,
      provider: 'wordpress',
      apiKeyId: null,
      oauthTokenId: storedToken?.id,
    })
  })

  it('returns a 400 OAUTH_INVALID envelope when the PKCE verifier does not match', async () => {
    const db = await createDb()
    const { accountId } = await seedAccountAndPlugin(db)
    const client = await registerTranslateClient(db)
    const verifier = 'pkce-verifier-good'
    const challenge = await deriveCodeChallengeS256(verifier)
    const authorized = await issueAuthorizationCode(db, {
      clientId: client.id,
      redirectUri: client.redirectUris[0]!,
      codeChallenge: challenge,
      codeChallengeMethod: 'S256',
      scope: 'plugin:translate',
    })
    const app = createOauthRoute({ db })
    app.onError(onError)

    const response = await app.request(
      createRequest(
        new URLSearchParams({
          grant_type: 'authorization_code',
          code: authorized.code,
          code_verifier: 'pkce-verifier-bad',
          client_id: client.id,
          account_id: accountId,
        }),
      ),
    )

    expect(response.status).toBe(400)
    await expect(readJson(response)).resolves.toEqual({
      error: {
        code: 'OAUTH_INVALID',
        message: 'oauth code verifier does not match the stored S256 challenge',
      },
    })
  })
})
