/**
 * Integration tests for GeoApiClient against a mock API server.
 * Covers the full flow from docs: register, login, collections, features.
 */
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { verifyAsync } from "@noble/ed25519";
import { base64UrlToBytes, bytesToBase64Url, textToBytes } from "../src/encoding";
import { GeoApiClient } from "../src/GeoApiClient";
import { generateKeyPair, signMessage } from "../src/keys";

class MemoryStorage {
  private data = new Map<string, string>();
  getItem(key: string): string | null {
    return this.data.get(key) ?? null;
  }
  setItem(key: string, value: string): void {
    this.data.set(key, value);
  }
  removeItem(key: string): void {
    this.data.delete(key);
  }
}

async function createMockServer(): Promise<{ url: string; server: ReturnType<typeof Bun.serve> }> {
  const serverKeypair = await generateKeyPair();
  const users = new Set<string>();
  const sessions = new Map<string, string>();
  let collectionId = 0;
  let featureId = 0;
  let assetId = 0;
  const collectionsByUser = new Map<string, Array<{ id: string; name: string }>>();
  const featuresByCollection = new Map<string, Array<{ id: string; geometry: unknown; properties: Record<string, unknown> }>>();
  const assetsByOwner = new Map<string, Array<{
    id: string;
    ownerKey: string;
    checksum: string;
    ext: string;
    kind: "image" | "3d";
    mimeType?: string;
    sizeBytes: number;
    objectKey: string;
    isPublic: boolean;
    createdAt: string;
    updatedAt: string;
  }>>();

  const server = Bun.serve({
    port: 0,
    async fetch(req) {
      const url = new URL(req.url);
      const path = url.pathname;
      const method = req.method;

      // GET /v1/service-key
      if (method === "GET" && path === "/v1/service-key") {
        return Response.json({ publicKey: serverKeypair.publicKey });
      }

      // POST /v1/auth/challenge
      if (method === "POST" && path === "/v1/auth/challenge") {
        const body = (await req.json()) as { publicKey?: string };
        if (!body?.publicKey) {
          return Response.json({ error: "missing publicKey" }, { status: 400 });
        }
        const nonce = bytesToBase64Url(crypto.getRandomValues(new Uint8Array(24)));
        return Response.json({ nonce, messageToSign: `login:${nonce}` });
      }

      // POST /v1/auth/register-by-signature
      if (method === "POST" && path === "/v1/auth/register-by-signature") {
        const body = (await req.json()) as { publicKey?: string; signature?: string };
        if (!body?.publicKey || !body?.signature) {
          return Response.json({ error: "missing fields" }, { status: 400 });
        }
        const ok = await verifyAsync(
          base64UrlToBytes(body.signature),
          textToBytes(serverKeypair.publicKey),
          base64UrlToBytes(body.publicKey)
        );
        if (!ok) {
          return Response.json({ error: "invalid signature" }, { status: 400 });
        }
        users.add(body.publicKey);
        return new Response(null, { status: 201 });
      }

      // POST /v1/auth/login
      if (method === "POST" && path === "/v1/auth/login") {
        const body = (await req.json()) as { publicKey?: string; nonce?: string; signature?: string };
        if (!body?.publicKey || !body?.nonce || !body?.signature) {
          return Response.json({ error: "missing fields" }, { status: 400 });
        }
        const msg = `login:${body.nonce}`;
        const ok = await verifyAsync(
          base64UrlToBytes(body.signature),
          textToBytes(msg),
          base64UrlToBytes(body.publicKey)
        );
        if (!ok) {
          return Response.json({ error: "invalid signature" }, { status: 401 });
        }
        if (!users.has(body.publicKey)) {
          return Response.json({ error: "user not registered" }, { status: 401 });
        }
        const token = bytesToBase64Url(crypto.getRandomValues(new Uint8Array(32)));
        sessions.set(token, body.publicKey);
        return Response.json({ accessToken: token });
      }

      // Bearer auth helper
      const auth = req.headers.get("Authorization");
      const token = auth?.startsWith("Bearer ") ? auth.slice(7) : null;
      const user = token ? sessions.get(token) : null;
      if (!user && path.startsWith("/v1/collections") && method !== "GET") {
        return Response.json({ error: "unauthorized" }, { status: 401 });
      }
      if (!user && path.startsWith("/v1/assets")) {
        return Response.json({ error: "unauthorized" }, { status: 401 });
      }

      // POST /v1/collections
      if (method === "POST" && path === "/v1/collections") {
        const body = (await req.json()) as { name?: string };
        collectionId++;
        const c = { id: `col-${collectionId}`, name: body?.name ?? "Unnamed" };
        const list = collectionsByUser.get(user!) ?? [];
        list.push(c);
        collectionsByUser.set(user!, list);
        return Response.json(c, { status: 201 });
      }

      // GET /v1/collections
      if (method === "GET" && path === "/v1/collections") {
        return Response.json({ collections: collectionsByUser.get(user ?? "") ?? [] });
      }

      // POST /v1/collections/:id/features
      if (method === "POST" && path.match(/^\/v1\/collections\/[^/]+\/features$/)) {
        const colId = path.split("/")[3]!;
        const owned = (collectionsByUser.get(user!) ?? []).some((c) => c.id === colId);
        if (!owned) {
          return Response.json({ error: "forbidden" }, { status: 403 });
        }
        const body = (await req.json()) as { geometry?: unknown; properties?: unknown };
        featureId++;
        const f = {
          id: `feat-${featureId}`,
          geometry: body?.geometry ?? { type: "Point", coordinates: [0, 0] },
          properties: (body?.properties as Record<string, unknown>) ?? {},
        };
        const list = featuresByCollection.get(colId) ?? [];
        list.push(f);
        featuresByCollection.set(colId, list);
        return Response.json(f, { status: 201 });
      }

      // GET /v1/collections/:id/features
      if (method === "GET" && path.match(/^\/v1\/collections\/[^/]+\/features$/)) {
        const colId = path.split("/")[3]!;
        const features = featuresByCollection.get(colId) ?? [];
        const ownerAssets = assetsByOwner.get(user ?? "") ?? [];
        const withAssets = features.map((f) => {
          const assets = ownerAssets
            .filter((a) => (f.properties.assets as Array<{ id: string }> | undefined)?.some((x) => x.id === a.id))
            .map((a) => ({
              id: a.id,
              kind: a.kind,
              checksum: a.checksum,
              ext: a.ext,
              isPublic: a.isPublic,
              link: `/v1/assets/${a.id}/download`,
            }));
          return { ...f, properties: { ...f.properties, assets } };
        });
        return Response.json({ features: withAssets });
      }

      // POST /v1/assets
      if (method === "POST" && path === "/v1/assets") {
        const body = (await req.json()) as {
          featureId?: string;
          checksum?: string;
          ext?: string;
          kind?: "image" | "3d";
          mimeType?: string;
          sizeBytes?: number;
          name?: string;
          description?: string;
          isPublic?: boolean;
        };
        if (!body.featureId || !body.checksum || !body.ext || !body.kind) {
          return Response.json({ error: "missing fields" }, { status: 400 });
        }
        const ownerAssets = assetsByOwner.get(user!) ?? [];
        const existing = ownerAssets.find(
          (a) => a.checksum === body.checksum.toLowerCase() && a.ext === body.ext.toLowerCase()
        );
        const now = new Date().toISOString();
        const asset =
          existing ??
          {
            id: `asset-${++assetId}`,
            ownerKey: user!,
            checksum: body.checksum.toLowerCase(),
            ext: body.ext.toLowerCase(),
            kind: body.kind,
            mimeType: body.mimeType,
            sizeBytes: body.sizeBytes ?? 0,
            objectKey: `${user!}/${body.checksum.toLowerCase()}.${body.ext.toLowerCase()}`,
            isPublic: body.isPublic ?? true,
            createdAt: now,
            updatedAt: now,
          };
        if (!existing) {
          ownerAssets.push(asset);
          assetsByOwner.set(user!, ownerAssets);
        }
        for (const featureList of featuresByCollection.values()) {
          for (const f of featureList) {
            if (f.id === body.featureId) {
              const oldAssets = Array.isArray(f.properties.assets) ? (f.properties.assets as Array<{ id: string }>) : [];
              if (!oldAssets.some((x) => x.id === asset.id)) {
                f.properties.assets = [...oldAssets, { id: asset.id, name: body.name, description: body.description }];
              }
            }
          }
        }
        return Response.json({ asset, link: `/v1/assets/${asset.id}/download` }, { status: existing ? 200 : 201 });
      }

      // POST /v1/assets/:id/signed-upload
      if (method === "POST" && path.match(/^\/v1\/assets\/[^/]+\/signed-upload$/)) {
        const id = path.split("/")[3]!;
        return Response.json({ url: `/v1/assets/${id}/upload`, method: "PUT" });
      }

      // PUT /v1/assets/:id/upload
      if (method === "PUT" && path.match(/^\/v1\/assets\/[^/]+\/upload$/)) {
        return new Response(null, { status: 204 });
      }

      // PATCH /v1/assets/:id
      if (method === "PATCH" && path.match(/^\/v1\/assets\/[^/]+$/)) {
        const id = path.split("/")[3]!;
        const body = (await req.json()) as { isPublic?: boolean };
        const ownerAssets = assetsByOwner.get(user!) ?? [];
        const asset = ownerAssets.find((a) => a.id === id);
        if (!asset) {
          return Response.json({ error: "not found" }, { status: 404 });
        }
        asset.isPublic = Boolean(body.isPublic);
        asset.updatedAt = new Date().toISOString();
        return Response.json({ asset, link: `/v1/assets/${asset.id}/download` });
      }

      return new Response("Not Found", { status: 404 });
    },
  });

  const url = `http://localhost:${server.port}`;
  return { url, server };
}

describe("GeoApiClient integration (docs flow)", () => {
  let mock: { url: string; server: ReturnType<typeof Bun.serve> };
  let client: GeoApiClient;
  let storage: MemoryStorage;

  beforeAll(async () => {
    mock = await createMockServer();
    storage = new MemoryStorage();
    client = new GeoApiClient(mock.url, storage);
  });

  afterAll(() => {
    mock.server.stop();
  });

  test("getServicePublicKey fetches server public key", async () => {
    const { publicKey } = await client.getServicePublicKey();
    expect(publicKey.length).toBeGreaterThan(10);
  });

  test("full flow: ensureKeys -> register -> login -> createCollection -> createPointFeature -> listFeatures", async () => {
    const keys = await client.ensureKeysInStorage();
    expect(keys.publicKey).toBeDefined();
    expect(keys.privateKey).toBeDefined();

    await client.registerBySigningServiceKey(keys.publicKey, keys.privateKey);

    const token = await client.loginWithSignature(keys.publicKey, keys.privateKey);
    expect(token.length).toBeGreaterThan(10);
    client.setAccessToken(token);

    const created = await client.createCollection("My Places");
    expect(created.id).toBeDefined();
    expect(created.name).toBe("My Places");

    const feature = await client.createPointFeature(created.id, -16.6291, 28.4636, {
      name: "Santa Cruz",
    });
    expect(feature.id).toBeDefined();

    const { features } = await client.listFeatures(created.id);
    expect(Array.isArray(features)).toBe(true);
  });

  test("asset flow: create/link -> signed upload -> toggle visibility", async () => {
    const keys = await client.ensureKeysInStorage();
    await client.registerBySigningServiceKey(keys.publicKey, keys.privateKey);
    await client.loginWithSignature(keys.publicKey, keys.privateKey);

    const created = await client.createCollection("3D");
    const feature = await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Palm Tree Point" });

    const createdAsset = await client.createOrLinkAsset({
      featureId: feature.id,
      checksum: "ABCDEF0123",
      ext: "glb",
      kind: "3d",
      mimeType: "model/gltf-binary",
      sizeBytes: 1024,
      name: "Palm Tree",
      description: "Low poly",
      isPublic: true,
    });
    expect(createdAsset.asset.id).toBeDefined();
    expect(createdAsset.link).toBe(`/v1/assets/${createdAsset.asset.id}/download`);
    expect(client.resolveRelativeLink(createdAsset.link)).toContain(`/v1/assets/${createdAsset.asset.id}/download`);

    const upload = await client.getAssetSignedUploadUrl(createdAsset.asset.id, "model/gltf-binary");
    expect(upload.method).toBe("PUT");
    expect(upload.url).toContain(`/v1/assets/${createdAsset.asset.id}/upload`);

    await client.uploadAssetBinary(createdAsset.asset.id, new Blob(["fake-glb"]), "model/gltf-binary");

    const toggled = await client.setAssetVisibility(createdAsset.asset.id, false);
    expect(toggled.asset.isPublic).toBe(false);

    const listed = await client.listFeatures(created.id);
    const first = listed.features[0];
    const assets = first.properties?.assets ?? [];
    expect(assets.length).toBeGreaterThan(0);
    expect(assets[0]?.id).toBe(createdAsset.asset.id);
  });
});
