import { createClient } from "@supabase/supabase-js";
import { testClient } from "hono/testing";
import type { Channel, StreamChat } from "stream-chat";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createConfig } from "../../config.js";
import { MiddlewareManager } from "../../middlewares/middleware.js";
import { getMainRouter } from "../../routers/index.js";
import { getTestUser } from "../helpers/dbSetup.js";

// Mock the stream-chat module
vi.mock("stream-chat", () => {
  const mockChannel = {
    create: vi.fn().mockResolvedValue(undefined),
    sendMessage: vi.fn().mockResolvedValue(undefined),
  };

  const mockStreamChatInstance = {
    channel: vi.fn(() => mockChannel),
    upsertUser: vi.fn().mockResolvedValue({ users: {} }),
  };

  return {
    StreamChat: {
      getInstance: vi.fn(() => mockStreamChatInstance),
    },
  };
});

// Mock nodemailer
const mockSendMail = vi.fn();
vi.mock("nodemailer", () => ({
  default: {
    createTransport: vi.fn(() => ({
      sendMail: mockSendMail,
    })),
  },
}));

describe("Booking Endpoint", () => {
  // In test mode, createConfig() reads from .env.test
  const config = createConfig();
  MiddlewareManager.initialize(config);

  const app = getMainRouter(config);
  // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
  const client = testClient(app) as any;

  const ownerUser = getTestUser("owner");
  const supabase = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_ROLE_KEY);

  let ownerShortId: string;
  let eventTypeStandardName: string;
  const createdBookings: string[] = [];
  const createdTablos: string[] = [];
  const createdUsers: string[] = [];

  // Get references to the mocked functions for assertions
  let mockStreamChat: StreamChat;
  let mockChannel: Channel;

  beforeAll(async () => {
    // Get references to the mocked instances
    const { StreamChat } = await import("stream-chat");
    mockStreamChat = StreamChat.getInstance("test_api_key", "test_api_secret");
    mockChannel = mockStreamChat.channel("messaging", "test_channel_id");

    // Get owner's short_user_id
    const { data: ownerProfile } = await supabase
      .from("profiles")
      .select("short_user_id")
      .eq("id", ownerUser.userId)
      .single();

    ownerShortId = ownerProfile?.short_user_id || "test_owner_short_id";

    // Use existing test event type that's already in the database
    eventTypeStandardName = "test-consultation";
  });

  beforeEach(() => {
    // Reset all mocks before each test
    vi.clearAllMocks();
    mockSendMail.mockResolvedValue({ messageId: "test-message-id" });
  });

  afterAll(async () => {
    // Delete events (bookings)
    if (createdBookings.length > 0) {
      const { error: eventsError } = await supabase
        .from("events")
        .delete()
        .in("tablo_id", createdBookings);
      if (eventsError) {
        console.warn("Warning: Failed to delete test events:", eventsError.message);
      }
    }

    // Delete tablo_access records for created tablos
    if (createdTablos.length > 0) {
      const { error: accessError } = await supabase
        .from("tablo_access")
        .delete()
        .in("tablo_id", createdTablos);
      if (accessError) {
        console.warn("Warning: Failed to delete tablo access:", accessError.message);
      }
    }

    // Delete created tablos
    if (createdTablos.length > 0) {
      const { error: tablosError } = await supabase.from("tablos").delete().in("id", createdTablos);
      if (tablosError) {
        console.warn("Warning: Failed to delete test tablos:", tablosError.message);
      }
    }

    // Delete created users from auth
    for (const userId of createdUsers) {
      const { error: userError } = await supabase.auth.admin.deleteUser(userId);
      if (userError) {
        console.warn(`Warning: Failed to delete user ${userId}:`, userError.message);
      }
    }
  });

  describe("POST /book/slot - Book a Slot", () => {
    it("should require owner_short_id", async () => {
      const res = await client.book.slot.$post(
        {
          json: {
            event_type_standard_name: eventTypeStandardName,
            event_details: {
              start_date: "2025-01-15",
              start_time: "10:00",
              end_time: "11:00",
            },
            user_details: {
              name: "Test User",
              email: "test@example.com",
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      expect(res.status).toBe(400);
      const data = await res.json();
      expect(data.error).toBe("owner_id is required");
    });

    it("should require event_type_standard_name", async () => {
      const res = await client.book.slot.$post(
        {
          json: {
            owner_short_id: ownerShortId,
            event_details: {
              start_date: "2025-01-15",
              start_time: "10:00",
              end_time: "11:00",
            },
            user_details: {
              name: "Test User",
              email: "test@example.com",
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      expect(res.status).toBe(400);
      const data = await res.json();
      expect(data.error).toBe("event is required");
    });

    it("should reject invalid owner_short_id", async () => {
      const res = await client.book.slot.$post(
        {
          json: {
            owner_short_id: "invalid_short_id",
            event_type_standard_name: eventTypeStandardName,
            event_details: {
              start_date: "2025-01-15",
              start_time: "10:00",
              end_time: "11:00",
            },
            user_details: {
              name: "Test User",
              email: "test@example.com",
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      expect(res.status).toBe(400);
      const data = await res.json();
      expect(data.error).toBe("owner_id is incorrect");
    });

    it("should reject invalid event_type_standard_name", async () => {
      const res = await client.book.slot.$post(
        {
          json: {
            owner_short_id: ownerShortId,
            event_type_standard_name: "non-existent-event-type",
            event_details: {
              start_date: "2025-01-15",
              start_time: "10:00",
              end_time: "11:00",
            },
            user_details: {
              name: "Test User",
              email: "test@example.com",
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      expect(res.status).toBe(404);
      const data = await res.json();
      expect(data.error).toBe("Event type not found");
    });

    it("should prevent booking with yourself", async () => {
      const { data: ownerProfile } = await supabase
        .from("profiles")
        .select("email")
        .eq("id", ownerUser.userId)
        .single();

      const res = await client.book.slot.$post(
        {
          json: {
            owner_short_id: ownerShortId,
            event_type_standard_name: eventTypeStandardName,
            event_details: {
              start_date: "2025-01-15",
              start_time: "10:00",
              end_time: "11:00",
            },
            user_details: {
              name: "Test Owner",
              email: ownerProfile.email,
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      expect(res.status).toBe(400);
      const data = await res.json();
      expect(data.error).toBe("You cannot create a tablo with yourself");
    });

    it("should successfully book a slot and create a new user account", async () => {
      const newUserEmail = `test_booker_${Date.now()}@example.com`;

      const res = await client.book.slot.$post(
        {
          json: {
            owner_short_id: ownerShortId,
            event_type_standard_name: eventTypeStandardName,
            event_details: {
              start_date: "2025-02-15",
              start_time: "10:00",
              end_time: "10:30",
            },
            user_details: {
              name: "New Test Booker",
              email: newUserEmail,
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      if (res.status !== 200) {
        const errorData = await res.json();
        console.error("Error response:", errorData);
      }
      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toBe("Booking successful");
      expect(data.tablo_id).toBeDefined();
      expect(data.hasCreatedAccount).toBe(true);
      expect(data.email).toBe(newUserEmail);

      // Track created resources for cleanup
      createdTablos.push(data.tablo_id);
      createdBookings.push(data.tablo_id);

      // Get created user ID for cleanup
      const { data: userProfile } = await supabase
        .from("profiles")
        .select("id")
        .eq("email", newUserEmail)
        .single();
      if (userProfile) {
        createdUsers.push(userProfile.id);
      }

      // Verify Stream Chat channel was created
      expect(mockChannel.create).toHaveBeenCalledTimes(1);
      expect(mockChannel.sendMessage).toHaveBeenCalledTimes(1);

      // Verify emails were sent (3 emails: welcome to new user, one to owner, one to booker)
      expect(mockSendMail).toHaveBeenCalledTimes(3);

      // Get the calls to sendMail
      const emailCalls = mockSendMail.mock.calls;

      // Extract the username from email for verification (used as name by createInvitedUser)
      const expectedUserName = newUserEmail.split("@")[0];

      // First email should be the welcome email to the new user (from createInvitedUser)
      const welcomeEmailCall = emailCalls[0][0];
      expect(welcomeEmailCall.to).toBe(newUserEmail);
      expect(welcomeEmailCall.subject).toContain("Vous avez été invité sur XTablo");
      expect(welcomeEmailCall.html).toContain("Mot de passe");

      // Second email should be to the owner
      const ownerEmailCall = emailCalls[1][0];
      expect(ownerEmailCall.subject).toContain("Nouveau tablo créé");
      expect(ownerEmailCall.html).toContain(expectedUserName); // Name is derived from email prefix
      expect(ownerEmailCall.html).toContain(newUserEmail);

      // Third email should be to the booker (booking confirmation)
      const bookerEmailCall = emailCalls[2][0];
      expect(bookerEmailCall.to).toBe(newUserEmail);
      expect(bookerEmailCall.subject).toContain("Réservation confirmée");
      expect(bookerEmailCall.html).toContain("2025-02-15");
      expect(bookerEmailCall.html).toContain("10:00");
    });

    it("should successfully book a slot with existing user", async () => {
      // Create a test user first
      const existingUserEmail = `test_existing_${Date.now()}@example.com`;
      const { data: authData } = await supabase.auth.admin.createUser({
        email: existingUserEmail,
        password: "test_password_123",
        email_confirm: true,
        user_metadata: {
          name: "Existing Test User",
        },
      });

      if (authData.user) {
        createdUsers.push(authData.user.id);
      }

      const res = await client.book.slot.$post(
        {
          json: {
            owner_short_id: ownerShortId,
            event_type_standard_name: eventTypeStandardName,
            event_details: {
              start_date: "2025-02-16",
              start_time: "14:00",
              end_time: "14:30",
            },
            user_details: {
              name: "Existing Test User",
              email: existingUserEmail,
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.message).toBe("Booking successful");
      expect(data.tablo_id).toBeDefined();
      expect(data.hasCreatedAccount).toBe(false);
      expect(data.email).toBe(existingUserEmail);

      // Track created resources for cleanup
      createdTablos.push(data.tablo_id);
      createdBookings.push(data.tablo_id);

      // Verify Stream Chat channel was created
      expect(mockChannel.create).toHaveBeenCalledTimes(1);
      expect(mockChannel.sendMessage).toHaveBeenCalledTimes(1);

      // Verify emails were sent (2 emails: one to owner, one to booker)
      expect(mockSendMail).toHaveBeenCalledTimes(2);

      // Get the calls to sendMail
      const emailCalls = mockSendMail.mock.calls;

      // First email should be to the owner
      const ownerEmailCall = emailCalls[0][0];
      expect(ownerEmailCall.subject).toContain("Nouveau tablo créé");
      expect(ownerEmailCall.html).toContain("Existing Test User");

      // Second email should be to the existing user (booker)
      const bookerEmailCall = emailCalls[1][0];
      expect(bookerEmailCall.to).toBe(existingUserEmail);
      expect(bookerEmailCall.subject).toContain("Réservation confirmée");
      expect(bookerEmailCall.html).toContain("2025-02-16");
      expect(bookerEmailCall.html).toContain("14:00");
    });

    it("should reuse existing tablo for subsequent bookings with same user", async () => {
      const existingUserEmail = `test_reuse_${Date.now()}@example.com`;

      // First booking
      const res1 = await client.book.slot.$post(
        {
          json: {
            owner_short_id: ownerShortId,
            event_type_standard_name: eventTypeStandardName,
            event_details: {
              start_date: "2025-03-15",
              start_time: "10:00",
              end_time: "10:30",
            },
            user_details: {
              name: "Test Reuse User",
              email: existingUserEmail,
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      expect(res1.status).toBe(200);
      const data1 = await res1.json();
      const firstTabloId = data1.tablo_id;

      // Track created resources
      createdTablos.push(firstTabloId);
      createdBookings.push(firstTabloId);
      const { data: userProfile } = await supabase
        .from("profiles")
        .select("id")
        .eq("email", existingUserEmail)
        .single();
      if (userProfile) {
        createdUsers.push(userProfile.id);
      }

      // Verify emails were sent for first booking (3 emails: welcome, owner, booker)
      // New account created, so includes welcome email
      expect(mockSendMail).toHaveBeenCalledTimes(3);

      vi.clearAllMocks();
      mockSendMail.mockResolvedValue({ messageId: "test-message-id-2" });

      // Second booking with same user
      const res2 = await client.book.slot.$post(
        {
          json: {
            owner_short_id: ownerShortId,
            event_type_standard_name: eventTypeStandardName,
            event_details: {
              start_date: "2025-03-16",
              start_time: "11:00",
              end_time: "11:30",
            },
            user_details: {
              name: "Test Reuse User",
              email: existingUserEmail,
            },
          },
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      expect(res2.status).toBe(200);
      const data2 = await res2.json();

      // Should reuse the same tablo
      expect(data2.tablo_id).toBe(firstTabloId);
      expect(data2.hasCreatedAccount).toBe(false);

      // Stream Chat channel should still be created for the second booking
      expect(mockChannel.create).toHaveBeenCalledTimes(1);

      // Verify emails were sent for second booking (2 emails)
      expect(mockSendMail).toHaveBeenCalledTimes(2);

      // Get the calls to sendMail for second booking
      const emailCalls = mockSendMail.mock.calls;

      // Extract the username from email for verification (used as name by createInvitedUser)
      const expectedUserName = existingUserEmail.split("@")[0];

      // First email should be to the owner
      const ownerEmailCall = emailCalls[0][0];
      expect(ownerEmailCall.subject).toContain("Nouveau tablo créé");
      expect(ownerEmailCall.html).toContain(expectedUserName);

      // Second email should be to the reuse user
      const bookerEmailCall = emailCalls[1][0];
      expect(bookerEmailCall.to).toBe(existingUserEmail);
      expect(bookerEmailCall.subject).toContain("Réservation confirmée");
      expect(bookerEmailCall.html).toContain("2025-03-16");
      expect(bookerEmailCall.html).toContain("11:00");
    });
  });
});
