import { createClient, type ApiClient, type ApiResponse } from './client.js'
import { type TestResult, printSuiteHeader, printTestResult } from './reporter.js'

export interface TestContext {
  api: ApiClient
  token: string
  baseUrl: string
  test: (name: string, optsOrFn: { tags?: string[] } | (() => Promise<void>), maybeFn?: () => Promise<void>) => void
  assert: {
    status: (res: ApiResponse, expected: number) => void
    equal: <T>(actual: T, expected: T, msg?: string) => void
    notEqual: <T>(actual: T, expected: T, msg?: string) => void
    ok: (value: unknown, msg?: string) => void
    truthy: (value: unknown, msg?: string) => void
    falsy: (value: unknown, msg?: string) => void
    includes: (arr: unknown[], value: unknown, msg?: string) => void
    contains: (str: string, sub: string, msg?: string) => void
    greaterThan: (actual: number, expected: number, msg?: string) => void
    throws: (fn: () => unknown, msg?: string) => void
  }
}

class AssertionError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'AssertionError'
  }
}

function makeAssert() {
  return {
    status(res: ApiResponse, expected: number) {
      if (res.status !== expected) {
        const body = typeof res.data === 'object' ? JSON.stringify(res.data) : String(res.data)
        throw new AssertionError(`Expected status ${expected}, got ${res.status}: ${body.slice(0, 200)}`)
      }
    },
    equal<T>(actual: T, expected: T, msg?: string) {
      if (actual !== expected) {
        throw new AssertionError(msg ?? `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
      }
    },
    notEqual<T>(actual: T, expected: T, msg?: string) {
      if (actual === expected) {
        throw new AssertionError(msg ?? `Expected value to not equal ${JSON.stringify(expected)}`)
      }
    },
    ok(value: unknown, msg?: string) {
      if (!value) throw new AssertionError(msg ?? `Expected truthy, got ${JSON.stringify(value)}`)
    },
    truthy(value: unknown, msg?: string) {
      if (!value) throw new AssertionError(msg ?? `Expected truthy, got ${JSON.stringify(value)}`)
    },
    falsy(value: unknown, msg?: string) {
      if (value) throw new AssertionError(msg ?? `Expected falsy, got ${JSON.stringify(value)}`)
    },
    includes(arr: unknown[], value: unknown, msg?: string) {
      if (!arr.includes(value)) {
        throw new AssertionError(msg ?? `Expected array to include ${JSON.stringify(value)}`)
      }
    },
    contains(str: string, sub: string, msg?: string) {
      if (!str.includes(sub)) {
        throw new AssertionError(msg ?? `Expected "${str}" to contain "${sub}"`)
      }
    },
    greaterThan(actual: number, expected: number, msg?: string) {
      if (actual <= expected) {
        throw new AssertionError(msg ?? `Expected ${actual} > ${expected}`)
      }
    },
    throws(fn: () => unknown, msg?: string) {
      try {
        fn()
        throw new AssertionError(msg ?? 'Expected function to throw')
      } catch (e) {
        if (e instanceof AssertionError) throw e
      }
    },
  }
}

interface RegisteredTest {
  name: string
  tags: string[]
  fn: () => Promise<void>
}

export interface SuiteDefinition {
  name: string
  tags: string[]
  setup: (ctx: TestContext) => void | Promise<void>
}

const registeredSuites: SuiteDefinition[] = []

export function suite(
  name: string,
  optsOrFn: { tags?: string[] } | ((ctx: TestContext) => void | Promise<void>),
  maybeFn?: (ctx: TestContext) => void | Promise<void>,
) {
  const opts = typeof optsOrFn === 'function' ? {} : optsOrFn
  const fn = typeof optsOrFn === 'function' ? optsOrFn : maybeFn!
  registeredSuites.push({ name, tags: opts.tags ?? [], setup: fn })
}

export function getSuites(): SuiteDefinition[] {
  return registeredSuites
}

export async function runSuite(
  suiteDef: SuiteDefinition,
  baseUrl: string,
  token: string,
  filterTags?: string[],
): Promise<TestResult[]> {
  const tests: RegisteredTest[] = []
  const api = createClient(baseUrl, token)

  const ctx: TestContext = {
    api,
    token,
    baseUrl,
    assert: makeAssert(),
    test(name, optsOrFn, maybeFn) {
      const opts = typeof optsOrFn === 'function' ? {} : optsOrFn
      const fn = typeof optsOrFn === 'function' ? optsOrFn : maybeFn!
      tests.push({ name, tags: (opts as { tags?: string[] }).tags ?? [], fn })
    },
  }

  await suiteDef.setup(ctx)

  printSuiteHeader(suiteDef.name)

  const results: TestResult[] = []
  for (const test of tests) {
    // Tag filtering
    if (filterTags && filterTags.length > 0) {
      const allTags = [...suiteDef.tags, ...test.tags]
      if (!filterTags.some((t) => allTags.includes(t))) continue
    }

    const start = performance.now()
    try {
      await test.fn()
      const duration = Math.round(performance.now() - start)
      const result: TestResult = { name: test.name, suite: suiteDef.name, tags: test.tags, passed: true, duration }
      results.push(result)
      printTestResult(result)
    } catch (err) {
      const duration = Math.round(performance.now() - start)
      const error = err instanceof Error ? err.message : String(err)
      const result: TestResult = { name: test.name, suite: suiteDef.name, tags: test.tags, passed: false, duration, error }
      results.push(result)
      printTestResult(result)
    }
  }

  console.log('')
  return results
}
