import { Injectable } from '@nestjs/common';
import { appForbidden } from '../../../shared/common/app-error';
import { PrismaService } from '../../../shared/prisma/prisma.service';
import { AgentsService } from '../../agent/agents.service';
import { BetsService } from '../../betting/bets.service';
import { SettlementService } from '../../settlement/settlement.service';
import { WalletService } from '../../ledger/wallet.service';
import { SMOKE_SUITE_META, SMOKE_TEST_CASES, type SmokeTestCaseDef } from './smoke-test.cases';
import { BET_FLOW_PROBE_COUNT, createBetFlowProbes } from './smoke-test.bet-flow-probes';
import { CONFIG_PROBE_COUNT, createConfigProbes } from './smoke-test.config-probes';
import { createDatabaseProbes, DATABASE_PROBE_COUNT } from './smoke-test.db-probes';
import {
  beginSmokeSteps,
  drainSmokeSteps,
  formatStepsForResult,
} from './smoke-test.helpers';
import type {
  SmokeTestCaseResult,
  SmokeTestRunSummary,
  SmokeTestSuiteInfo,
} from './smoke-test.types';

@Injectable()
export class SmokeTestService {
  private lastRun: SmokeTestRunSummary | null = null;

  constructor(
    private prisma: PrismaService,
    private wallet: WalletService,
    private bets: BetsService,
    private settlement: SettlementService,
    private agents: AgentsService,
  ) {}

  isAllowed(): boolean {
    if (process.env.ALLOW_SMOKE_TESTS === 'true') return true;
    return process.env.NODE_ENV !== 'production';
  }

  assertAllowed() {
    if (!this.isAllowed()) {
      throw appForbidden('SMOKE_TESTS_FORBIDDEN');
    }
  }

  listSuites(): SmokeTestSuiteInfo[] {
    const counts = new Map<string, number>();
    for (const c of SMOKE_TEST_CASES) {
      counts.set(c.suite, (counts.get(c.suite) ?? 0) + 1);
    }
    counts.set('database', DATABASE_PROBE_COUNT);
    counts.set('bet-flow', BET_FLOW_PROBE_COUNT);
    counts.set('config', CONFIG_PROBE_COUNT);

    return [...counts.entries()].map(([id, caseCount]) => ({
      id,
      name: SMOKE_SUITE_META[id]?.name ?? id,
      description: SMOKE_SUITE_META[id]?.description ?? '',
      caseCount,
    }));
  }

  listCases(suites?: string[]) {
    const allow = suites?.length ? new Set(suites) : null;
    return SMOKE_TEST_CASES.filter((c) => !allow || allow.has(c.suite)).map(
      ({ id, suite, name, description, uatRef }) => ({ id, suite, name, description, uatRef }),
    );
  }

  getLastRun() {
    return this.lastRun;
  }

  async run(suites?: string[], operatorId?: bigint): Promise<SmokeTestRunSummary> {
    const started = Date.now();
    const runId = `SMOKE-${started}-${operatorId?.toString() ?? '0'}`;
    const allow = suites?.length ? new Set(suites) : null;

    const staticCases = SMOKE_TEST_CASES.filter((c) => !allow || allow.has(c.suite));
    const runDb = !allow || allow.has('database');
    const runBetFlow = !allow || allow.has('bet-flow');
    const runConfig = !allow || allow.has('config');

    const results: SmokeTestCaseResult[] = [];

    for (const testCase of staticCases) {
      results.push(await this.executeCase(testCase));
    }

    if (runDb) {
      for (const probe of createDatabaseProbes(this.prisma)) {
        results.push(await this.executeCase(probe));
      }
    }

    if (runBetFlow) {
      for (const probe of createBetFlowProbes({
        prisma: this.prisma,
        wallet: this.wallet,
        bets: this.bets,
        settlement: this.settlement,
        agents: this.agents,
      })) {
        results.push(await this.executeCase(probe));
      }
    }

    if (runConfig) {
      for (const probe of createConfigProbes({
        prisma: this.prisma,
        agents: this.agents,
      })) {
        results.push(await this.executeCase(probe));
      }
    }

    const finished = Date.now();
    const passed = results.filter((r) => r.status === 'PASS').length;
    const failed = results.filter((r) => r.status === 'FAIL').length;
    const skipped = results.filter((r) => r.status === 'SKIP').length;

    const summary: SmokeTestRunSummary = {
      runId,
      startedAt: new Date(started).toISOString(),
      finishedAt: new Date(finished).toISOString(),
      durationMs: finished - started,
      total: results.length,
      passed,
      failed,
      skipped,
      suites: [...new Set(results.map((r) => r.suite))],
      results,
    };

    this.lastRun = summary;
    return summary;
  }

  private async executeCase(testCase: SmokeTestCaseDef): Promise<SmokeTestCaseResult> {
    const t0 = performance.now();
    const base = {
      id: testCase.id,
      suite: testCase.suite,
      name: testCase.name,
      description: testCase.description,
      uatRef: testCase.uatRef,
    };

    beginSmokeSteps();
    try {
      await testCase.run();
      const steps = drainSmokeSteps();
      const durationMs = Math.max(0.01, Math.round((performance.now() - t0) * 100) / 100);
      return {
        ...base,
        status: 'PASS',
        durationMs,
        stepCount: steps.length,
        message: steps.length ? `${steps.length} steps passed` : 'OK',
        details: formatStepsForResult(steps),
      };
    } catch (err) {
      const steps = drainSmokeSteps();
      const message = err instanceof Error ? err.message : String(err);
      const durationMs = Math.max(0.01, Math.round((performance.now() - t0) * 100) / 100);
      return {
        ...base,
        status: 'FAIL',
        durationMs,
        stepCount: steps.length,
        error: message,
        details: formatStepsForResult(steps),
      };
    }
  }
}
