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

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { IncomingMessage, ServerResponse } from "node:http";
import { Socket } from "node:net";

import {
  createNostrProfileHttpHandler,
  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
): IncomingMessage {
  const socket = new Socket();
  const req = new IncomingMessage(socket);
  req.method = method;
  req.url = url;
  req.headers = { host: "localhost:3000" };

  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 socket = new Socket();
  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) 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,
  };
}

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

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

  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", () => {
    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();

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

      await handler(req, res);

      expect(res._getStatusCode()).toBe(200);
      const data = JSON.parse(res._getData());
      expect(data.ok).toBe(true);
      expect(data.eventId).toBe("event123");
      expect(data.successes).toContain("wss://relay.damus.io");
      expect(data.persisted).toBe(true);
      expect(ctx.updateConfigProfile).toHaveBeenCalled();
    });

    it("rejects private IP in picture URL (SSRF protection)", async () => {
      const ctx = createMockContext();
      const handler = createNostrProfileHttpHandler(ctx);
      const req = createMockRequest("PUT", "/api/channels/nostr/default/profile", {
        name: "hacker",
        picture: "https://127.0.0.1/evil.jpg",
      });
      const res = createMockResponse();

      await handler(req, res);

      expect(res._getStatusCode()).toBe(400);
      const data = JSON.parse(res._getData());
      expect(data.ok).toBe(false);
      expect(data.error).toContain("private");
    });

    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);

      expect(res._getStatusCode()).toBe(400);
      const data = JSON.parse(res._getData());
      expect(data.ok).toBe(false);
      // 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);

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

      // 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) {
          expect(res._getStatusCode()).toBe(200);
        } else {
          expect(res._getStatusCode()).toBe(429);
          const data = JSON.parse(res._getData());
          expect(data.error).toContain("Rate limit");
        }
      }
    });
  });

  describe("POST /api/channels/nostr/:accountId/profile/import", () => {
    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();

      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",
      });

      await handler(req, res);

      expect(res._getStatusCode()).toBe(200);
      const data = JSON.parse(res._getData());
      expect(data.ok).toBe(true);
      expect(data.imported.name).toBe("imported");
      expect(data.saved).toBe(false); // autoMerge not requested
    });

    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();

      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",
      });

      await handler(req, res);

      expect(res._getStatusCode()).toBe(200);
      const data = JSON.parse(res._getData());
      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");
    });
  });
});
