import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getTableName } from 'drizzle-orm'
import { getTableConfig } from 'drizzle-orm/pg-core'
import { refreshTokens, tenantMemberships, tenants, users } from '../../src/schema'

const mockCompleteEmailVerificationTx = vi.fn()

vi.mock('../../src/queries/auth-writes', () => ({
  completeEmailVerificationTx: (...args: unknown[]) => mockCompleteEmailVerificationTx(...args),
}))

import {
  E2eFixtureConflictError,
  E2E_FIXTURE_TTL_MS,
  cleanupE2eRoleFixture,
  provisionE2eRoleFixture,
} from '../../src/queries/e2e-role-fixtures'

const tenantId = '00000000-0000-4000-8000-000000000001'
const ownerId = '00000000-0000-4000-8000-000000000002'
const roleNames = ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER', 'CONTRACTOR', 'ACCOUNTANT']

function isTable(table: unknown, name: string): boolean {
  return getTableName(table as Parameters<typeof getTableName>[0]) === name
}

function builder(result: unknown) {
  const terminal = async () => result
  const chain: Record<string, unknown> = {}
  for (const method of ['from', 'innerJoin']) chain[method] = vi.fn(() => chain)
  chain.where = vi.fn(() => chain)
  chain.limit = vi.fn(terminal)
  chain.then = (resolve: (value: unknown) => unknown) => resolve(result)
  return chain
}

function provisionHarness(staleCandidates: unknown[] = []) {
  let selectCount = 0
  let userInsertCount = 0
  const mutations: Array<{ kind: string; table: unknown }> = []
  const tx = {
    select: vi.fn(() => {
      selectCount += 1
      if (selectCount === 1) return builder(staleCandidates)
      return builder(roleNames.map((name, index) => ({ id: `role-${index}`, name })))
    }),
    insert: vi.fn((table: unknown) => {
      mutations.push({ kind: 'insert', table })
      return {
        values: vi.fn(() => {
          if (table === users) {
            userInsertCount += 1
            const rows = userInsertCount === 1
              ? [{ id: ownerId }]
              : roleNames.slice(1).map((role, index) => ({
                  id: `user-${index}`,
                  email: `e2e+ci-42-${role.toLowerCase()}@zync.invalid`,
                }))
            return { returning: vi.fn(async () => rows) }
          }
          return Promise.resolve()
        }),
      }
    }),
    update: vi.fn((table: unknown) => {
      mutations.push({ kind: 'update', table })
      return { set: vi.fn(() => ({ where: vi.fn(async () => undefined) })) }
    }),
    delete: vi.fn((table: unknown) => {
      mutations.push({ kind: 'delete', table })
      return { where: vi.fn(async () => undefined) }
    }),
  }
  const db = {
    transaction: vi.fn(async (callback: (inner: typeof tx) => Promise<unknown>) => callback(tx)),
  }
  return { db, tx, mutations }
}

describe('E2E role fixture queries', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockCompleteEmailVerificationTx.mockResolvedValue({
      tenantId,
      tenantSlug: 'e2e-ci-42-deadbeef',
      tier: 'freelancer',
      role: 'OWNER',
      roleId: 'role-0',
    })
  })

  it('provisions owner, tenant seed, users, memberships, and audits in one transaction', async () => {
    const { db, tx, mutations } = provisionHarness()
    const now = new Date('2026-07-13T00:00:00.000Z')

    const fixture = await provisionE2eRoleFixture(db as never, {
      runId: 'ci-42',
      passwordHash: 'hash',
      now,
    })

    expect(db.transaction).toHaveBeenCalledTimes(1)
    expect(mockCompleteEmailVerificationTx).toHaveBeenCalledWith(tx, {
      userId: ownerId,
      tenantName: 'E2E ci-42',
      markEmailVerified: false,
    })
    expect(mutations.filter(({ table }) => table === users)).toHaveLength(2)
    expect(mutations).toContainEqual({ kind: 'update', table: tenants })
    expect(mutations).toContainEqual({ kind: 'insert', table: tenantMemberships })
    expect(mutations.filter(({ table }) => isTable(table, 'audit_log'))).toHaveLength(1)
    expect(fixture.users.map(({ role }) => role)).toEqual(roleNames)
  })

  it('maps the unique email constraint race to a namespace conflict', async () => {
    const conflict = Object.assign(new Error('duplicate'), { code: '23505' })
    const db = { transaction: vi.fn().mockRejectedValue(conflict) }

    await expect(provisionE2eRoleFixture(db as never, {
      runId: 'ci-42',
      passwordHash: 'hash',
    })).rejects.toBeInstanceOf(E2eFixtureConflictError)
  })

  it('opportunistically removes only validated stale reserved owner namespaces', async () => {
    const { db, mutations } = provisionHarness([
      {
        tenantId: '00000000-0000-4000-8000-000000000010',
        tenantName: 'E2E abandoned-run',
        ownerId: '00000000-0000-4000-8000-000000000011',
        ownerEmail: 'e2e+abandoned-run-owner@zync.invalid',
      },
      {
        tenantId: '00000000-0000-4000-8000-000000000020',
        tenantName: 'Ordinary tenant',
        ownerId: '00000000-0000-4000-8000-000000000021',
        ownerEmail: 'ordinary-user@example.com',
      },
    ])

    await provisionE2eRoleFixture(db as never, {
      runId: 'ci-42',
      passwordHash: 'hash',
      now: new Date(E2E_FIXTURE_TTL_MS * 2),
    })

    expect(mutations.filter(({ kind, table }) => kind === 'delete' && table === tenants)).toHaveLength(1)
    expect(mutations.filter(({ kind, table }) => kind === 'delete' && table === users)).toHaveLength(1)
    expect(mutations.filter(({ table }) => isTable(table, 'audit_log'))).toHaveLength(2)
  })

  it('finds and deletes the exact namespace inside one cleanup transaction', async () => {
    const exact = [{
      tenantId,
      tenantName: 'E2E ci-42',
      ownerId,
      ownerEmail: 'e2e+ci-42-owner@zync.invalid',
    }]
    const tx = {
      select: vi.fn(() => builder(exact)),
      insert: vi.fn((_table: unknown) => ({ values: vi.fn(async () => undefined) })),
      delete: vi.fn((_table: unknown) => ({ where: vi.fn(async () => undefined) })),
    }
    const db = {
      transaction: vi.fn(async (callback: (inner: typeof tx) => Promise<boolean>) => callback(tx)),
    }

    await expect(cleanupE2eRoleFixture(db as never, 'ci-42')).resolves.toBe(true)
    expect(db.transaction).toHaveBeenCalledTimes(1)
    expect(tx.select).toHaveBeenCalledTimes(1)
    expect(tx.insert.mock.calls.some(([table]) => isTable(table, 'audit_log'))).toBe(true)
    expect(tx.delete).toHaveBeenCalledWith(tenants)
    expect(tx.delete).toHaveBeenCalledWith(users)
  })

  it('refuses cleanup when the reserved owner belongs to a non-fixture tenant', async () => {
    const tx = {
      select: vi.fn(() => builder([{
        tenantId,
        tenantName: 'Customer production tenant',
        ownerId,
        ownerEmail: 'e2e+ci-42-owner@zync.invalid',
      }])),
    }
    const db = {
      transaction: vi.fn(async (callback: (inner: typeof tx) => Promise<boolean>) => callback(tx)),
    }

    await expect(cleanupE2eRoleFixture(db as never, 'ci-42')).resolves.toBe(false)
  })

  it('relies only on declared cascades for memberships and login sessions', () => {
    const membershipCascades = getTableConfig(tenantMemberships).foreignKeys
      .filter(({ onDelete }) => onDelete === 'cascade')
    const sessionCascades = getTableConfig(refreshTokens).foreignKeys
      .filter(({ onDelete }) => onDelete === 'cascade')

    expect(membershipCascades).toHaveLength(2)
    expect(sessionCascades).toHaveLength(2)
  })
})
