import { assertSemanticMutationKind } from "./operators.js";
import type {
  ContractTest,
  ContractTestResult,
  MutationCoverageFinding,
  MutationRoundResult,
  MutationRoundSummary,
  RunMutationRoundInput,
  SemanticMutation,
} from "./types.js";

function assertNonEmptyString(value: string, field: string): void {
  if (value.trim().length === 0) {
    throw new Error(`${field} must be a non-empty string`);
  }
}

function validateContractTest<T>(test: ContractTest<T>): void {
  assertNonEmptyString(test.id, "contractTests[].id");
  assertNonEmptyString(test.contractClass, "contractTests[].contractClass");
  if (typeof test.run !== "function") {
    throw new Error("contractTests[].run must be a function");
  }
}

function validateMutation<T>(mutation: SemanticMutation<T>): void {
  assertNonEmptyString(mutation.id, "mutations[].id");
  assertSemanticMutationKind(mutation.kind);
  if (typeof mutation.apply !== "function") {
    throw new Error("mutations[].apply must be a function");
  }
}

async function runContractTest<T>(
  test: ContractTest<T>,
  impl: T,
): Promise<ContractTestResult> {
  const result = await test.run(impl);
  if (typeof result.passed !== "boolean" || typeof result.testId !== "string") {
    throw new Error(`contract test ${test.id} returned invalid result shape`);
  }
  return result;
}

function primaryContractClass<T>(contractTests: readonly ContractTest<T>[]): string {
  const classes = new Set(contractTests.map((test) => test.contractClass));
  if (classes.size !== 1) {
    throw new Error("contractTests must share one contractClass per mutation round");
  }
  return contractTests[0]?.contractClass ?? "unknown";
}

export async function runMutationRound<T>(
  input: RunMutationRoundInput<T>,
): Promise<MutationRoundSummary> {
  if (input.contractTests.length === 0) {
    throw new Error("contractTests must not be empty");
  }
  if (input.mutations.length === 0) {
    throw new Error("mutations must not be empty");
  }

  for (const test of input.contractTests) {
    validateContractTest(test);
  }
  for (const mutation of input.mutations) {
    validateMutation(mutation);
  }

  const contractClass = primaryContractClass(input.contractTests);
  const results: MutationRoundResult[] = [];
  const coverageFindings: MutationCoverageFinding[] = [];

  for (const mutation of input.mutations) {
    const mutated = mutation.apply(input.base);
    const testResults = await Promise.all(
      input.contractTests.map((test) => runContractTest(test, mutated)),
    );
    const failedTests = testResults.filter((result) => !result.passed).map((result) => result.testId);
    const killed = failedTests.length > 0;

    results.push({
      mutationId: mutation.id,
      kind: mutation.kind,
      killed,
      failedTests,
    });

    if (!killed) {
      coverageFindings.push({
        kind: "surviving-semantic-mutant",
        mutationId: mutation.id,
        mutationKind: mutation.kind,
        contractClass,
        evidence: {
          passedTests: testResults.map((result) => result.testId),
        },
      });
    }
  }

  const killedMutants = results.filter((result) => result.killed).length;
  const totalMutants = results.length;

  return {
    results,
    totalMutants,
    killedMutants,
    score: totalMutants === 0 ? 0 : killedMutants / totalMutants,
    coverageFindings,
  };
}
