import type { ProtocolScenario } from "../../../../universal/fixtures/protocol/types.js";
import {
  PROTOCOL_CASE_KINDS,
  type ProtocolCase,
  type ProtocolCaseKind,
  type ProtocolContractSpec,
  type ProtocolUniRule,
  type TransferValidationScenario,
} 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 assertCaseKind(value: string): asserts value is ProtocolCaseKind {
  if (!(PROTOCOL_CASE_KINDS as readonly string[]).includes(value)) {
    throw new Error(`unknown protocol case kind: ${value}`);
  }
}

function chunkBoundaries(frames: string[]): string[][] {
  const fullStream = `${frames.join("\n")}\n`;
  const firstFrame = frames[0];
  if (frames.length > 1 && firstFrame !== undefined) {
    const midpoint = Math.max(1, Math.floor(firstFrame.length / 2));
    return [
      [fullStream],
      frames.map((line) => `${line}\n`),
      [
        firstFrame.slice(0, midpoint),
        `${firstFrame.slice(midpoint)}\n${frames.slice(1).join("\n")}\n`,
      ],
    ];
  }
  return [[fullStream], frames.map((line) => `${line}\n`)];
}

function baseStreamScenario(
  spec: ProtocolContractSpec,
  scenarioId: string,
  constraints: ProtocolScenario & { kind: "stream-decode" } extends { constraints: infer C }
    ? C
    : never,
  chunkings: Array<{
    chunkingId: string;
    chunks: string[];
    terminalCount?: number;
    events?: Array<{ seq?: number; type: string; payload?: unknown }>;
    parseErrors?: string[];
  }>,
): ProtocolScenario {
  return {
    scenarioId,
    kind: "stream-decode",
    frames: spec.frames,
    terminalType: spec.terminalType,
    constraints,
    chunkings: chunkings.map((chunking) => ({
      chunkingId: chunking.chunkingId,
      chunks: chunking.chunks,
      observation: {
        events: chunking.events ?? [],
        terminalCount: chunking.terminalCount ?? 0,
        parseErrors: chunking.parseErrors ?? [],
      },
    })),
  };
}

function caseRule(kind: ProtocolCaseKind): ProtocolUniRule {
  switch (kind) {
    case "chunk-boundary":
    case "flush-timing":
    case "duplicate-frame":
    case "missing-terminal-frame":
    case "late-terminal-frame":
      return "UNI-060";
    case "stdout-contamination":
    case "stderr-contamination":
      return "UNI-061";
    case "malformed-encoding":
    case "version-skew":
      return "UNI-062";
    case "transfer-validation":
      return "UNI-063";
    default: {
      const exhaustive: never = kind;
      throw new Error(`unsupported protocol case kind: ${String(exhaustive)}`);
    }
  }
}

function buildStreamCases(spec: ProtocolContractSpec): ProtocolCase[] {
  const cases: ProtocolCase[] = [];
  const boundaries = chunkBoundaries(spec.frames);
  const canonicalEvents = spec.frames.map((frame, index) => {
    try {
      const parsed = JSON.parse(frame) as { seq?: number; type: string; payload?: unknown };
      return {
        seq: parsed.seq ?? index + 1,
        type: parsed.type,
        ...(parsed.payload !== undefined ? { payload: parsed.payload } : {}),
      };
    } catch {
      return { seq: index + 1, type: "data" };
    }
  });

  cases.push({
    caseId: `${spec.id}:chunk-boundary`,
    kind: "chunk-boundary",
    rule: "UNI-060",
    description: "decoded events remain identical across chunk boundaries",
    scenario: baseStreamScenario(
      spec,
      "chunk-boundary-independence",
      {
        preserveOrder: true,
        requireSingleTerminal: true,
        chunkBoundaryIndependent: true,
        requireParseable: true,
      },
      boundaries.map((chunks, index) => ({
        chunkingId: `chunking-${String(index)}`,
        chunks,
        events: canonicalEvents,
        terminalCount: canonicalEvents.some((event) => event.type === spec.terminalType) ? 1 : 0,
      })),
    ),
  });

  cases.push({
    caseId: `${spec.id}:flush-timing`,
    kind: "flush-timing",
    rule: "UNI-060",
    description: "flush timing does not change decoded terminal record count",
    scenario: baseStreamScenario(
      spec,
      "flush-timing",
      {
        requireSingleTerminal: true,
        chunkBoundaryIndependent: true,
      },
      [
        {
          chunkingId: "immediate-flush",
          chunks: boundaries[0] ?? [`${spec.frames.join("\n")}\n`],
          events: canonicalEvents,
          terminalCount: canonicalEvents.some((event) => event.type === spec.terminalType) ? 1 : 0,
        },
        {
          chunkingId: "delayed-flush",
          chunks: boundaries.at(-1) ?? [`${spec.frames.join("\n")}\n`],
          events: canonicalEvents,
          terminalCount: canonicalEvents.some((event) => event.type === spec.terminalType) ? 1 : 0,
        },
      ],
    ),
  });

  const terminalFrames = spec.frames.filter((frame) => frame.includes(`"type":"${spec.terminalType}"`));
  if (terminalFrames.length > 0) {
    cases.push({
      caseId: `${spec.id}:duplicate-frame`,
      kind: "duplicate-frame",
      rule: "UNI-060",
      description: "duplicate terminal frame is rejected",
      scenario: baseStreamScenario(
        spec,
        "duplicate-terminal-frame",
        { requireSingleTerminal: true },
        [
          {
            chunkingId: "duplicate-terminal",
            chunks: [`${[...spec.frames, terminalFrames[0] ?? spec.frames.at(-1) ?? ""].join("\n")}\n`],
            events: [
              ...canonicalEvents,
              { seq: canonicalEvents.length + 1, type: spec.terminalType, payload: { status: "ok" } },
            ],
            terminalCount: 2,
          },
        ],
      ),
    });
  }

  cases.push({
    caseId: `${spec.id}:missing-terminal-frame`,
    kind: "missing-terminal-frame",
    rule: "UNI-060",
    description: "missing terminal frame fails closure",
    scenario: baseStreamScenario(
      spec,
      "missing-terminal-frame",
      { requireSingleTerminal: true },
      [
        {
          chunkingId: "without-terminal",
          chunks: [
            `${spec.frames.filter((frame) => !frame.includes(`"type":"${spec.terminalType}"`)).join("\n")}\n`,
          ],
          events: canonicalEvents.filter((event) => event.type !== spec.terminalType),
          terminalCount: 0,
        },
      ],
    ),
  });

  cases.push({
    caseId: `${spec.id}:late-terminal-frame`,
    kind: "late-terminal-frame",
    rule: "UNI-060",
    description: "late terminal frame after post-terminal traffic is detected",
    scenario: baseStreamScenario(
      spec,
      "late-terminal-frame",
      { requireSingleTerminal: true, preserveOrder: true },
      [
        {
          chunkingId: "late-terminal",
          chunks: [
            `${spec.frames.join("\n")}\n{"seq":99,"type":"progress","payload":{"pct":1}}\n{"seq":100,"type":"${spec.terminalType}","payload":{"status":"late"}}\n`,
          ],
          events: [
            ...canonicalEvents,
            { seq: 99, type: "progress", payload: { pct: 1 } },
            { seq: 100, type: spec.terminalType, payload: { status: "late" } },
          ],
          terminalCount: 2,
        },
      ],
    ),
  });

  return cases;
}

function buildStdoutCases(spec: ProtocolContractSpec): ProtocolCase[] {
  const fullStream = `${spec.frames.join("\n")}\n`;
  return [
    {
      caseId: `${spec.id}:stdout-contamination`,
      kind: "stdout-contamination",
      rule: "UNI-061",
      description: "human-readable text must not contaminate machine-readable stdout",
      scenario: {
        scenarioId: "stdout-contamination",
        kind: "stdout-purity",
        stdout: `${spec.frames[0] ?? ""}\n[INFO] worker started\n${spec.frames.slice(1).join("\n")}\n`,
        stderr: "[worker] ready\n",
        declaredSideChannels: ["stderr"],
        terminalType: spec.terminalType,
        observation: {
          protocolFrames: spec.frames,
          contaminations: [{ offset: (spec.frames[0]?.length ?? 0) + 1, text: "[INFO] worker started\n" }],
          partialFrames: 0,
          terminalCount: spec.frames.some((frame) => frame.includes(spec.terminalType)) ? 1 : 0,
          duplicateTerminalCount: 0,
          postTerminalEvents: 0,
        },
      },
    },
    {
      caseId: `${spec.id}:stderr-contamination`,
      kind: "stderr-contamination",
      rule: "UNI-061",
      description: "protocol frames must not be emitted on stderr when stdout owns the channel",
      scenario: {
        scenarioId: "stderr-protocol-leak",
        kind: "stdout-purity",
        stdout: fullStream,
        stderr: `${spec.frames[0] ?? ""}\n`,
        declaredSideChannels: ["stderr"],
        terminalType: spec.terminalType,
        observation: {
          protocolFrames: spec.frames,
          contaminations: [],
          partialFrames: 0,
          terminalCount: 1,
          duplicateTerminalCount: 0,
          postTerminalEvents: 0,
        },
      },
    },
  ];
}

function buildSerializationCases(spec: ProtocolContractSpec): ProtocolCase[] {
  if (spec.typedValue === undefined) {
    return [];
  }

  const contract = spec.compatibilityContract ?? {
    allowUnknownFields: false,
    rejectVersionSkew: true,
    expectedVersion: 1,
    charset: "utf-8" as const,
    newline: "lf" as const,
    bom: "forbid" as const,
    compression: "none" as const,
    rejectPartialWrites: true,
  };

  return [
    {
      caseId: `${spec.id}:malformed-encoding`,
      kind: "malformed-encoding",
      rule: "UNI-062",
      description: "malformed encoding violates explicit compatibility contract",
      scenario: {
        scenarioId: "malformed-encoding",
        kind: "serialization-round-trip",
        typedValue: spec.typedValue,
        compatibilityContract: contract,
        observation: {
          serialized: "\ufffd not-json\n",
          deserialized: spec.typedValue,
          metadata: {
            contentType: "application/json",
            hasBom: true,
            newlineStyle: "mixed",
          },
        },
      },
    },
    {
      caseId: `${spec.id}:version-skew`,
      kind: "version-skew",
      rule: "UNI-062",
      description: "version skew must follow explicit compatibility contract",
      scenario: {
        scenarioId: "version-skew",
        kind: "serialization-round-trip",
        typedValue: spec.typedValue,
        compatibilityContract: contract,
        observation: {
          serialized: JSON.stringify({ ...(spec.typedValue as object), version: 99 }),
          deserialized: { ...(spec.typedValue as object), version: 99 },
          metadata: {
            inputVersion: 99,
            outputVersion: 99,
          },
        },
      },
    },
  ];
}

function buildTransferCase(spec: ProtocolContractSpec): ProtocolCase[] {
  if (spec.transfer === undefined) {
    return [];
  }

  const transferScenario: TransferValidationScenario = {
    scenarioId: "transfer-validation",
    kind: "transfer-validation",
    spec: spec.transfer,
    observation: {
      bytes: "not-a-valid-payload",
      declaredMediaType: spec.transfer.declaredMediaType,
      observedMediaType: "application/octet-stream",
      ...(spec.transfer.filename !== undefined ? { filename: spec.transfer.filename } : {}),
      ...(spec.transfer.declaredLength !== undefined
        ? { declaredLength: spec.transfer.declaredLength }
        : {}),
      ...(spec.transfer.declaredLength !== undefined
        ? { observedLength: spec.transfer.declaredLength + 1 }
        : {}),
      truncated: true,
      ...(spec.transfer.checksum !== undefined ? { checksumMatches: false } : {}),
    },
  };

  return [
    {
      caseId: `${spec.id}:transfer-validation`,
      kind: "transfer-validation",
      rule: "UNI-063",
      description: "download/upload validates bytes, media type, length, and checksum contract",
      scenario: transferScenario,
    },
  ];
}

export function generateProtocolCases(spec: ProtocolContractSpec): ProtocolCase[] {
  assertNonEmptyString(spec.id, "id");
  assertNonEmptyString(spec.protocolVersion, "protocolVersion");
  assertNonEmptyString(spec.terminalType, "terminalType");
  if (!Array.isArray(spec.frames) || spec.frames.length === 0) {
    throw new Error("frames must be a non-empty array");
  }
  if (spec.frames.some((frame) => typeof frame !== "string" || frame.trim().length === 0)) {
    throw new Error("frames must contain non-empty strings");
  }

  const cases = [
    ...buildStreamCases(spec),
    ...buildStdoutCases(spec),
    ...buildSerializationCases(spec),
    ...buildTransferCase(spec),
  ];

  for (const entry of cases) {
    assertCaseKind(entry.kind);
    if (entry.rule !== caseRule(entry.kind)) {
      throw new Error(`protocol case ${entry.caseId} maps to unexpected rule ${entry.rule}`);
    }
  }

  return cases;
}

export function protocolCaseKindsForRule(rule: ProtocolUniRule): ProtocolCaseKind[] {
  return PROTOCOL_CASE_KINDS.filter((kind) => caseRule(kind) === rule);
}
