import assert from "node:assert/strict";
import { test } from "node:test";
import { gzipSync } from "node:zlib";
import type { DB, IssueSample } from "@superlog/db";
import type * as schema from "@superlog/db/schema";
import {
  findSourceMapArtifact,
  symbolicateIssueSample,
  symbolicateStacktraceWithArtifact,
  symbolicateTelemetrySample,
  symbolicationAttrsForSample,
} from "./symbolication.js";

const sourceMap = JSON.stringify({
  version: 3,
  file: "index.android.bundle",
  sources: ["app/index.tsx"],
  names: ["HomeScreen"],
  mappings: "AAAAA",
});

const artifact = {
  id: "artifact-1",
  projectId: "project-1",
  platform: "android",
  release: "juno@1.2.3",
  dist: null,
  debugId: null,
  bundleFile: "index.android.bundle",
  mapFile: "index.android.bundle.map",
  sourceMapHash: "a".repeat(64),
  sourceMapBytes: Buffer.byteLength(sourceMap),
  storageBucket: "source-map-bucket",
  storageKey: "source-maps/project-1/android/hash.map.gz",
  contentEncoding: "gzip",
  uploadedByOrgApiKeyId: null,
  createdAt: new Date("2026-06-02T00:00:00.000Z"),
  updatedAt: new Date("2026-06-02T00:00:00.000Z"),
} satisfies schema.SourceMapArtifact;

test("symbolicateStacktraceWithArtifact rewrites generated stack frames", () => {
  const result = symbolicateStacktraceWithArtifact({
    stacktrace: "TypeError: bad\n    at useMemoCache (index.android.bundle:1:1)",
    sourceMap,
    artifact,
  });

  assert.ok(result);
  assert.equal(result.stacktrace, "TypeError: bad\n    at HomeScreen (app/index.tsx:1:1)");
  assert.deepEqual(result.frames[0], {
    functionName: "HomeScreen",
    source: "app/index.tsx",
    line: 1,
    column: 1,
    generatedFile: "index.android.bundle",
    generatedLine: 1,
    generatedColumn: 1,
  });
});

test("symbolicateStacktraceWithArtifact infers original function names from source content", () => {
  const sourceMapWithoutNames = JSON.stringify({
    version: 3,
    file: "entry.js",
    sources: ["/app/(tabs)/index.tsx"],
    sourcesContent: [
      [
        "async function emitError() {",
        "  const superlog = await ensureSuperlog();",
        "  superlog?.captureException(new Error('Expo SDK demo error'), { component: 'home' });",
        "}",
      ].join("\n"),
    ],
    names: [],
    mappings: "AAAA",
  });

  const result = symbolicateStacktraceWithArtifact({
    stacktrace: "Error: Expo SDK demo error\n    at L (entry.js:1:1)",
    sourceMap: sourceMapWithoutNames,
    artifact: { ...artifact, platform: "web" },
  });

  assert.ok(result);
  assert.equal(
    result.stacktrace,
    "Error: Expo SDK demo error\n    at emitError (/app/(tabs)/index.tsx:1:1)",
  );
  assert.equal(result.frames[0]?.functionName, "emitError");
});

test("symbolicationAttrsForSample extracts release, platform, dist, and debug id", () => {
  const sample = {
    kind: "log",
    service: "juno",
    severity: "ERROR",
    message: "bad",
    body: "bad",
    exceptionType: "TypeError",
    topFrame: null,
    normalizedFrames: [],
    stacktrace: "TypeError: bad\n    at index.android.bundle:1:1",
    seenAt: "2026-06-02T00:00:00.000Z",
    logAttrs: {
      "service.version": "juno@1.2.3",
      "device.platform": "Android",
      "expo.update_id": "update-1",
      "sourcemap.debug_id": "debug-1",
    },
    resourceAttrs: null,
  } satisfies IssueSample;

  assert.deepEqual(symbolicationAttrsForSample(sample), {
    debugId: "debug-1",
    release: "juno@1.2.3",
    dist: "update-1",
    platform: "android",
  });
});

test("symbolicateIssueSample loads matching source map object and symbolicates", async () => {
  const sample = {
    kind: "log",
    service: "juno",
    severity: "ERROR",
    message: "bad",
    body: "bad",
    exceptionType: "TypeError",
    topFrame: null,
    normalizedFrames: [],
    stacktrace: "TypeError: bad\n    at useMemoCache (index.android.bundle:1:1)",
    seenAt: "2026-06-02T00:00:00.000Z",
    logAttrs: {
      "service.version": "juno@1.2.3",
      "device.platform": "android",
    },
    resourceAttrs: null,
  } satisfies IssueSample;
  const database = {
    query: {
      sourceMapArtifacts: {
        findFirst: async () => null,
        findMany: async () => [artifact],
      },
    },
  } as unknown as DB;

  const result = await symbolicateIssueSample({
    database,
    objectReader: {
      async getSourceMapObject(input) {
        assert.equal(input.bucket, "source-map-bucket");
        assert.equal(input.key, "source-maps/project-1/android/hash.map.gz");
        return gzipSync(sourceMap);
      },
    },
    projectId: "project-1",
    sample,
  });

  assert.ok(result);
  assert.equal(result.artifact.id, "artifact-1");
  assert.equal(result.frames[0]?.source, "app/index.tsx");
});

test("symbolicateTelemetrySample symbolicates logs without issue rows", async () => {
  const database = {
    query: {
      sourceMapArtifacts: {
        findFirst: async () => null,
        findMany: async () => [artifact],
      },
    },
  } as unknown as DB;

  const result = await symbolicateTelemetrySample({
    database,
    objectReader: {
      async getSourceMapObject(input) {
        assert.equal(input.bucket, "source-map-bucket");
        assert.equal(input.key, "source-maps/project-1/android/hash.map.gz");
        return gzipSync(sourceMap);
      },
    },
    projectId: "project-1",
    sample: {
      stacktrace: "TypeError: bad\n    at useMemoCache (index.android.bundle:1:1)",
      logAttrs: {
        "service.version": "juno@1.2.3",
        "device.platform": "android",
      },
      resourceAttrs: null,
    },
  });

  assert.ok(result);
  assert.equal(result.artifact.id, "artifact-1");
  assert.equal(result.frames[0]?.source, "app/index.tsx");
});

test("symbolicateTelemetrySample returns null when a log has no stacktrace", async () => {
  const result = await symbolicateTelemetrySample({
    database: {} as DB,
    objectReader: null,
    projectId: "project-1",
    sample: {
      stacktrace: null,
      logAttrs: null,
      resourceAttrs: null,
    },
  });

  assert.equal(result, null);
});

test("findSourceMapArtifact prefers artifact matching generated stack frame file", async () => {
  const entryArtifact = {
    ...artifact,
    id: "entry-artifact",
    platform: "web",
    bundleFile: "dist/_expo/static/js/web/entry-abc123.js",
    mapFile: "dist/_expo/static/js/web/entry-abc123.js.map",
    createdAt: new Date("2026-06-02T00:00:00.000Z"),
    updatedAt: new Date("2026-06-02T00:00:00.000Z"),
  } satisfies schema.SourceMapArtifact;
  const indexArtifact = {
    ...artifact,
    id: "index-artifact",
    platform: "web",
    bundleFile: "dist/_expo/static/js/web/index-def456.js",
    mapFile: "dist/_expo/static/js/web/index-def456.js.map",
    createdAt: new Date("2026-06-02T00:01:00.000Z"),
    updatedAt: new Date("2026-06-02T00:01:00.000Z"),
  } satisfies schema.SourceMapArtifact;
  const database = {
    query: {
      sourceMapArtifacts: {
        findFirst: async () => null,
        findMany: async () => [indexArtifact, entryArtifact],
      },
    },
  } as unknown as DB;

  const result = await findSourceMapArtifact({
    database,
    projectId: "project-1",
    attrs: {
      debugId: null,
      release: "juno@1.2.3",
      dist: null,
      platform: "web",
    },
    stacktrace:
      "TypeError: bad\n    at useMemoCache (https://app.example.com/_expo/static/js/web/entry-abc123.js:1:1)",
  });

  assert.equal(result?.id, "entry-artifact");
});

test("findSourceMapArtifact falls back to generated stack frame file when release is missing", async () => {
  const entryArtifact = {
    ...artifact,
    id: "entry-artifact",
    platform: "web",
    release: "expo-test-2@local",
    bundleFile: "dist-superlog/_expo/static/js/web/entry-90d9514ed4f128f73d2feaf2e08ff315.js",
    mapFile: "dist-superlog/_expo/static/js/web/entry-90d9514ed4f128f73d2feaf2e08ff315.js.map",
    createdAt: new Date("2026-06-02T00:00:00.000Z"),
    updatedAt: new Date("2026-06-02T00:00:00.000Z"),
  } satisfies schema.SourceMapArtifact;
  const database = {
    query: {
      sourceMapArtifacts: {
        findFirst: async () => null,
        findMany: async () => [entryArtifact],
      },
    },
  } as unknown as DB;

  const result = await findSourceMapArtifact({
    database,
    projectId: "project-1",
    attrs: {
      debugId: null,
      release: null,
      dist: null,
      platform: null,
    },
    stacktrace:
      "Error: Expo SDK demo error\n    at L (http://localhost:8093/_expo/static/js/web/entry-90d9514ed4f128f73d2feaf2e08ff315.js:1095:1077)",
  });

  assert.equal(result?.id, "entry-artifact");
});

test("findSourceMapArtifact falls back to generated stack frame file when release attrs do not match upload release", async () => {
  const entryArtifact = {
    ...artifact,
    id: "entry-artifact",
    platform: "web",
    release: "expo-test-2@local",
    dist: null,
    bundleFile: "dist-superlog/_expo/static/js/web/entry-90d9514ed4f128f73d2feaf2e08ff315.js",
    mapFile: "dist-superlog/_expo/static/js/web/entry-90d9514ed4f128f73d2feaf2e08ff315.js.map",
    createdAt: new Date("2026-06-02T00:00:00.000Z"),
    updatedAt: new Date("2026-06-02T00:00:00.000Z"),
  } satisfies schema.SourceMapArtifact;
  const database = {
    query: {
      sourceMapArtifacts: {
        findFirst: async () => null,
        findMany: async (_query: unknown) => [entryArtifact],
      },
    },
  } as unknown as DB;

  const result = await findSourceMapArtifact({
    database,
    projectId: "project-1",
    attrs: {
      debugId: null,
      release: "1.0.0",
      dist: "embedded",
      platform: "web",
    },
    stacktrace:
      "Error: Expo SDK demo error\n    at L (http://localhost:8093/_expo/static/js/web/entry-90d9514ed4f128f73d2feaf2e08ff315.js:1095:1077)",
  });

  assert.equal(result?.id, "entry-artifact");
});
