/**
 * Tests for Nostr Profile HTTP Handler
 */

import { IncomingMessage, ServerResponse } from "node:http";
import { Socket } from "node:net";
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
  clearNostrProfileRateLimitStateForTest,
  createNostrProfileHttpHandler,
  getNostrProfileRateLimitStateSizeForTest,
  isNostrProfileRateLimitedForTest,
  type NostrProfileHttpContext,
} from "./nostr-profile-http.js";

// Mock the channel exports
vi.mock("./channel.js", () => ({
  publishNostrProfile: vi.fn(),
  getNostrProfileState: vi.fn(),
}));

// Mock the import module
vi.mock("./nostr-profile-import.js", () => ({
  importProfileFromRelays: vi.fn(),
  mergeProfiles: vi.fn((local, imported) => ({ ...imported, ...local })),
}));

import { publishNostrProfile, getNostrProfileState } from "./channel.js";
import { importProfileFromRelays } from "./nostr-profile-import.js";

// ============================================================================
// Test Helpers
// ============================================================================

function createMockRequest(
  method: string,
  url: string,
  body?: unknown,
  opts?: { headers?: Record<string, string>; remoteAddress?: string },
): IncomingMessage {
  const socket = new Socket();
  Object.defineProperty(socket, "remoteAddress", {
    value: opts?.remoteAddress ?? "127.0.0.1",
    configurable: true,
  });
  const req = new IncomingMessage(socket);
  req.method = method;
  req.url = url;
  req.headers = { host: "localhost:3000", ...(opts?.headers ?? {}) };

  if (body) {
    const bodyStr = JSON.stringify(body);
    process.nextTick(() => {
      req.emit("data", Buffer.from(bodyStr));
      req.emit("end");
    });
  } else {
    process.nextTick(() => {
      req.emit("end");
    });
  }

  return req;
}

function createMockResponse(): ServerResponse & {
  _getData: () => string;
  _getStatusCode: () => number;
} {
  const res = new ServerResponse({} as IncomingMessage);

  let data = "";
  let statusCode = 200;

  res.write = function (chunk: unknown) {
    data += String(chunk);
    return true;
  };

  res.end = function (chunk?: unknown) {
    if (chunk) {
      // eslint-disable-next-line @typescript-eslint/no-base-to-string
      data += String(chunk);
    }
    return this;
  };

  Object.defineProperty(res, "statusCode", {
    get: () => statusCode,
    set: (code: number) => {
      statusCode = code;
    },
  });

  (res as unknown as { _getData: () => string })._getData = () => data;
  (res as unknown as { _getStatusCode: () => number })._getStatusCode = () => statusCode;

  return res as ServerResponse & { _getData: () => string; _getStatusCode: () => number };
}

function createMockContext(overrides?: Partial<NostrProfileHttpContext>): NostrProfileHttpContext {
  return {
    getConfigProfile: vi.fn().mockReturnValue(undefined),
    updateConfigProfile: vi.fn().mockResolvedValue(undefined),
    getAccountInfo: vi.fn().mockReturnValue({
      pubkey: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
      relays: ["wss://relay.damus.io"],
    }),
    log: {
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    },
    ...overrides,
  };
}

function expectOkResponse(res: ReturnType<typeof createMockResponse>) {
  expect(res._getStatusCode()).toBe(200);
  const data = JSON.parse(res._getData());
  expect(data.ok).toBe(true);
  return data;
}

function mockSuccessfulProfileImport() {
  vi.mocked(importProfileFromRelays).mockResolvedValue({
    ok: true,
    profile: {
      name: "imported",
      displayName: "Imported User",
    },
    event: {
      id: "evt123",
      pubkey: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
      created_at: 1234567890,
    },
    relaysQueried: ["wss://relay.damus.io"],
    sourceRelay: "wss://relay.damus.io",
  });
}

// ============================================================================
// Tests
// ============================================================================

describe("nostr-profile-http", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    clearNostrProfileRateLimitStateForTest();
  });

  describe("route matching", () => {
    it("returns false for non-nostr paths", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("GET", "/api/channels/telegram/profile");
      const res = createMockResponse();

      const result = await handler(req, res);

      expect(result).toBe(false);
    });

    it("returns false for paths without accountId", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("GET", "/api/channels/nostr/");
      const res = createMockResponse();

      const result = await handler(req, res);

      expect(result).toBe(false);
    });

    it("handles /api/channels/nostr/:accountId/profile", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("GET", "/api/channels/nostr/default/profile");
      const res = createMockResponse();

      vi.mocked(getNostrProfileState).mockResolvedValue(null);

      const result = await handler(req, res);

      expect(result).toBe(true);
    });
  });

  describe("GET /api/channels/nostr/:accountId/profile", () => {
    it("returns profile and publish state", async () => {
      const ctx = createMockContext({
        getConfigProfile: vi.fn().mockReturnValue({
          name: "testuser",
          displayName: "Test User",
        }),
      });
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("GET", "/api/channels/nostr/default/profile");
      const res = createMockResponse();

      vi.mocked(getNostrProfileState).mockResolvedValue({
        lastPublishedAt: 1234567890,
        lastPublishedEventId: "abc123",
        lastPublishResults: { "wss://relay.damus.io": "ok" },
      });

      await handler(req, res);

      expect(res._getStatusCode()).toBe(200);
      const data = JSON.parse(res._getData());
      expect(data.ok).toBe(true);
      expect(data.profile.name).toBe("testuser");
      expect(data.publishState.lastPublishedAt).toBe(1234567890);
    });
  });

  describe("PUT /api/channels/nostr/:accountId/profile", () => {
    function mockPublishSuccess() {
      vi.mocked(publishNostrProfile).mockResolvedValue({
        eventId: "event123",
        createdAt: 1234567890,
        successes: ["wss://relay.damus.io"],
        failures: [],
      });
    }

    function expectBadRequestResponse(res: ReturnType<typeof createMockResponse>) {
      expect(res._getStatusCode()).toBe(400);
      const data = JSON.parse(res._getData());
      expect(data.ok).toBe(false);
      return data;
    }

    async function expectPrivatePictureRejected(pictureUrl: string) {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("PUT", "/api/channels/nostr/default/profile", {
        name: "hacker",
        picture: pictureUrl,
      });
      const res = createMockResponse();

      await handler(req, res);

      const data = expectBadRequestResponse(res);
      expect(data.error).toContain("private");
    }

    it("validates profile and publishes", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("PUT", "/api/channels/nostr/default/profile", {
        name: "satoshi",
        displayName: "Satoshi Nakamoto",
        about: "Creator of Bitcoin",
      });
      const res = createMockResponse();

      mockPublishSuccess();

      await handler(req, res);

      const data = expectOkResponse(res);
      expect(data.eventId).toBe("event123");
      expect(data.successes).toContain("wss://relay.damus.io");
      expect(data.persisted).toBe(true);
      expect(ctx.updateConfigProfile).toHaveBeenCalled();
    });

    it("rejects profile mutation from non-loopback remote address", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest(
        "PUT",
        "/api/channels/nostr/default/profile",
        { name: "attacker" },
        { remoteAddress: "198.51.100.10" },
      );
      const res = createMockResponse();

      await handler(req, res);
      expect(res._getStatusCode()).toBe(403);
    });

    it("rejects cross-origin profile mutation attempts", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest(
        "PUT",
        "/api/channels/nostr/default/profile",
        { name: "attacker" },
        { headers: { origin: "https://evil.example" } },
      );
      const res = createMockResponse();

      await handler(req, res);
      expect(res._getStatusCode()).toBe(403);
    });

    it("rejects profile mutation with cross-site sec-fetch-site header", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest(
        "PUT",
        "/api/channels/nostr/default/profile",
        { name: "attacker" },
        { headers: { "sec-fetch-site": "cross-site" } },
      );
      const res = createMockResponse();

      await handler(req, res);
      expect(res._getStatusCode()).toBe(403);
    });

    it("rejects profile mutation when forwarded client ip is non-loopback", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest(
        "PUT",
        "/api/channels/nostr/default/profile",
        { name: "attacker" },
        { headers: { "x-forwarded-for": "203.0.113.99, 127.0.0.1" } },
      );
      const res = createMockResponse();

      await handler(req, res);
      expect(res._getStatusCode()).toBe(403);
    });

    it("rejects private IP in picture URL (SSRF protection)", async () => {
      await expectPrivatePictureRejected("https://127.0.0.1/evil.jpg");
    });

    it("rejects ISATAP-embedded private IPv4 in picture URL", async () => {
      await expectPrivatePictureRejected("https://[2001:db8:1234::5efe:127.0.0.1]/evil.jpg");
    });

    it("rejects non-https URLs", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("PUT", "/api/channels/nostr/default/profile", {
        name: "test",
        picture: "http://example.com/pic.jpg",
      });
      const res = createMockResponse();

      await handler(req, res);

      const data = expectBadRequestResponse(res);
      // The schema validation catches non-https URLs before SSRF check
      expect(data.error).toBe("Validation failed");
      expect(data.details).toBeDefined();
      expect(data.details.some((d: string) => d.includes("https"))).toBe(true);
    });

    it("does not persist if all relays fail", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("PUT", "/api/channels/nostr/default/profile", {
        name: "test",
      });
      const res = createMockResponse();

      vi.mocked(publishNostrProfile).mockResolvedValue({
        eventId: "event123",
        createdAt: 1234567890,
        successes: [],
        failures: [{ relay: "wss://relay.damus.io", error: "timeout" }],
      });

      await handler(req, res);

      expect(res._getStatusCode()).toBe(200);
      const data = JSON.parse(res._getData());
      expect(data.persisted).toBe(false);
      expect(ctx.updateConfigProfile).not.toHaveBeenCalled();
    });

    it("enforces rate limiting", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);

      mockPublishSuccess();

      // Make 6 requests (limit is 5/min)
      for (let i = 0; i < 6; i++) {
        const req = createMockRequest("PUT", "/api/channels/nostr/rate-test/profile", {
          name: `user${i}`,
        });
        const res = createMockResponse();
        await handler(req, res);

        if (i < 5) {
          expectOkResponse(res);
        } else {
          expect(res._getStatusCode()).toBe(429);
          const data = JSON.parse(res._getData());
          expect(data.error).toContain("Rate limit");
        }
      }
    });

    it("caps tracked rate-limit keys to prevent unbounded growth", () => {
      const now = 1_000_000;
      for (let i = 0; i < 2_500; i += 1) {
        isNostrProfileRateLimitedForTest(`rate-cap-${i}`, now);
      }
      expect(getNostrProfileRateLimitStateSizeForTest()).toBeLessThanOrEqual(2_048);
    });

    it("prunes stale rate-limit keys after the window elapses", () => {
      const now = 2_000_000;
      for (let i = 0; i < 100; i += 1) {
        isNostrProfileRateLimitedForTest(`rate-stale-${i}`, now);
      }
      expect(getNostrProfileRateLimitStateSizeForTest()).toBe(100);

      isNostrProfileRateLimitedForTest("fresh", now + 60_001);
      expect(getNostrProfileRateLimitStateSizeForTest()).toBe(1);
    });
  });

  describe("POST /api/channels/nostr/:accountId/profile/import", () => {
    function expectImportSuccessResponse(res: ReturnType<typeof createMockResponse>) {
      const data = expectOkResponse(res);
      expect(data.imported.name).toBe("imported");
      return data;
    }

    it("imports profile from relays", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("POST", "/api/channels/nostr/default/profile/import", {});
      const res = createMockResponse();

      mockSuccessfulProfileImport();

      await handler(req, res);

      const data = expectImportSuccessResponse(res);
      expect(data.saved).toBe(false); // autoMerge not requested
    });

    it("rejects import mutation from non-loopback remote address", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest(
        "POST",
        "/api/channels/nostr/default/profile/import",
        {},
        { remoteAddress: "203.0.113.10" },
      );
      const res = createMockResponse();

      await handler(req, res);
      expect(res._getStatusCode()).toBe(403);
    });

    it("rejects cross-origin import mutation attempts", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest(
        "POST",
        "/api/channels/nostr/default/profile/import",
        {},
        { headers: { origin: "https://evil.example" } },
      );
      const res = createMockResponse();

      await handler(req, res);
      expect(res._getStatusCode()).toBe(403);
    });

    it("rejects import mutation when x-real-ip is non-loopback", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest(
        "POST",
        "/api/channels/nostr/default/profile/import",
        {},
        { headers: { "x-real-ip": "198.51.100.55" } },
      );
      const res = createMockResponse();

      await handler(req, res);
      expect(res._getStatusCode()).toBe(403);
    });

    it("auto-merges when requested", async () => {
      const ctx = createMockContext({
        getConfigProfile: vi.fn().mockReturnValue({ about: "local bio" }),
      });
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("POST", "/api/channels/nostr/default/profile/import", {
        autoMerge: true,
      });
      const res = createMockResponse();

      mockSuccessfulProfileImport();

      await handler(req, res);

      const data = expectImportSuccessResponse(res);
      expect(data.saved).toBe(true);
      expect(ctx.updateConfigProfile).toHaveBeenCalled();
    });

    it("returns error when account not found", async () => {
      const ctx = createMockContext({
        getAccountInfo: vi.fn().mockReturnValue(null),
      });
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("POST", "/api/channels/nostr/unknown/profile/import", {});
      const res = createMockResponse();

      await handler(req, res);

      expect(res._getStatusCode()).toBe(404);
      const data = JSON.parse(res._getData());
      expect(data.error).toContain("not found");
    });
  });
});
