import { describe, test, expect, mock, beforeEach } from "bun:test";
import type { GraphMailMessage } from "./utils";

/**
 * Integration tests for email channel server logic.
 * Tests the message processing pipeline without live Graph API calls.
 */

// Build a realistic GraphMailMessage for testing
function buildTestMessage(overrides?: Partial<GraphMailMessage>): GraphMailMessage {
  return {
    id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    conversationId: "conv-test-123",
    subject: "Test appointment request",
    bodyPreview: "Hi, I'd like to schedule an appointment for next week.",
    body: {
      contentType: "html",
      content:
        "<p>Hi,</p><p>I'd like to schedule an appointment for next week.</p><p>Thanks,<br>John</p>",
    },
    from: {
      emailAddress: { name: "John Doe", address: "john@example.com" },
    },
    toRecipients: [
      {
        emailAddress: {
          name: "Gautam Bhargava",
          address: "gautam@exulthealthcare.com",
        },
      },
    ],
    receivedDateTime: new Date().toISOString(),
    importance: "normal",
    hasAttachments: false,
    isRead: false,
    ...overrides,
  };
}

describe("message processing", () => {
  test("builds correct MCP notification meta (all strings)", () => {
    const msg = buildTestMessage();
    // Simulate what processMessage builds
    const meta: Record<string, string> = {
      source: "email-channel",
      sender_email: msg.from.emailAddress.address,
      sender_name: msg.from.emailAddress.name,
      subject: msg.subject,
      has_attachments: String(msg.hasAttachments),
      attachment_paths: JSON.stringify([]),
      received_at: msg.receivedDateTime,
      importance: msg.importance ?? "normal",
      is_read: String(msg.isRead),
      conversation_id: msg.conversationId ?? "",
      message_id: msg.id,
    };

    // ALL values must be strings — this is the critical invariant
    for (const [key, value] of Object.entries(meta)) {
      expect(typeof value).toBe("string");
    }
  });

  test("has_attachments is stringified, not boolean", () => {
    const msg = buildTestMessage({ hasAttachments: true });
    const hasAttachments = String(msg.hasAttachments);
    expect(hasAttachments).toBe("true");
    expect(typeof hasAttachments).toBe("string");
  });

  test("is_read is stringified, not boolean", () => {
    const msg = buildTestMessage({ isRead: false });
    const isRead = String(msg.isRead);
    expect(isRead).toBe("false");
    expect(typeof isRead).toBe("string");
  });
});

describe("anti-loop protection", () => {
  test("detects agent-sent messages by sender address", () => {
    const MAILBOX = "gautam@exulthealthcare.com";
    const msg = buildTestMessage({
      from: {
        emailAddress: {
          name: "Gautam Bhargava",
          address: "gautam@exulthealthcare.com",
        },
      },
    });
    const senderAddr = msg.from.emailAddress.address.toLowerCase();
    expect(senderAddr).toBe(MAILBOX.toLowerCase());
  });

  test("allows messages from external senders", () => {
    const MAILBOX = "gautam@exulthealthcare.com";
    const msg = buildTestMessage({
      from: {
        emailAddress: { name: "Patient", address: "patient@gmail.com" },
      },
    });
    const senderAddr = msg.from.emailAddress.address.toLowerCase();
    expect(senderAddr).not.toBe(MAILBOX.toLowerCase());
  });
});

describe("message dedup", () => {
  test("Set-based dedup prevents duplicate processing", () => {
    const seen = new Set<string>();
    const msgId = "AAMkAD-test-123";

    // First time: not seen
    expect(seen.has(msgId)).toBe(false);
    seen.add(msgId);

    // Second time: already seen
    expect(seen.has(msgId)).toBe(true);
  });

  test("dedup set eviction at capacity", () => {
    const MAX = 100;
    const seen = new Set<string>();

    for (let i = 0; i < MAX + 10; i++) {
      seen.add(`msg-${i}`);
      if (seen.size > MAX) {
        const first = seen.values().next().value;
        if (first) seen.delete(first);
      }
    }

    expect(seen.size).toBe(MAX);
    // Oldest should be evicted
    expect(seen.has("msg-0")).toBe(false);
    // Newest should remain
    expect(seen.has(`msg-${MAX + 9}`)).toBe(true);
  });
});

describe("address parsing", () => {
  test("parses comma-separated addresses", () => {
    const csv = "john@example.com, jane@example.com, bob@test.org";
    const parsed = csv
      .split(",")
      .map((a) => a.trim())
      .filter(Boolean)
      .map((address) => ({ emailAddress: { address } }));

    expect(parsed).toEqual([
      { emailAddress: { address: "john@example.com" } },
      { emailAddress: { address: "jane@example.com" } },
      { emailAddress: { address: "bob@test.org" } },
    ]);
  });

  test("handles single address", () => {
    const csv = "john@example.com";
    const parsed = csv
      .split(",")
      .map((a) => a.trim())
      .filter(Boolean)
      .map((address) => ({ emailAddress: { address } }));

    expect(parsed).toEqual([
      { emailAddress: { address: "john@example.com" } },
    ]);
  });

  test("handles empty/whitespace entries", () => {
    const csv = "john@example.com, , ,jane@example.com";
    const parsed = csv
      .split(",")
      .map((a) => a.trim())
      .filter(Boolean)
      .map((address) => ({ emailAddress: { address } }));

    expect(parsed).toEqual([
      { emailAddress: { address: "john@example.com" } },
      { emailAddress: { address: "jane@example.com" } },
    ]);
  });
});

describe("delta state", () => {
  test("serialization round-trip", () => {
    const state = {
      deltaLink: "https://graph.microsoft.com/v1.0/users/gautam@exulthealthcare.com/mailFolders/inbox/messages/delta?$deltatoken=abc123",
      lastPollTime: "2026-04-25T22:00:00.000Z",
    };
    const json = JSON.stringify(state);
    const parsed = JSON.parse(json);
    expect(parsed.deltaLink).toBe(state.deltaLink);
    expect(parsed.lastPollTime).toBe(state.lastPollTime);
  });

  test("null deltaLink on first run", () => {
    const state = { deltaLink: null, lastPollTime: new Date().toISOString() };
    expect(state.deltaLink).toBeNull();
  });
});
