/**
 * @jest-environment node
 */
import { DrizzleDatabase } from "../drizzle";
import { DatabaseConfig } from "../types";
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import * as schema from "@fitai/database";

describe("DrizzleDatabase", () => {
  let db: DrizzleDatabase;
  let sqlite: Database.Database;
  let drizzleDb: ReturnType<typeof drizzle>;

  beforeEach(async () => {
    // Create in-memory SQLite database
    sqlite = new Database(":memory:");
    drizzleDb = drizzle(sqlite, { schema });

    const config: DatabaseConfig = {
      type: "sqlite",
      connection: { filename: ":memory:" },
      options: { logging: false },
    };

    // Initialize DrizzleDatabase with the in-memory db
    db = new DrizzleDatabase(config, drizzleDb as any);
    await db.connect();
  });

  afterEach(async () => {
    await db.disconnect();
    sqlite.close();
  });

  describe("User Operations", () => {
    it("should create and retrieve a user", async () => {
      const newUser = {
        email: "test@example.com",
        firstName: "Test",
        lastName: "User",
        password: "password123",
        role: "client" as const,
      };

      const createdUser = await db.createUser(newUser);
      expect(createdUser).toBeDefined();
      expect(createdUser.id).toBeDefined();
      expect(createdUser.email).toBe(newUser.email);

      const fetchedUser = await db.getUserById(createdUser.id);
      expect(fetchedUser).toBeDefined();
      expect(fetchedUser?.id).toBe(createdUser.id);
      expect(fetchedUser?.email).toBe(createdUser.email);
      // SQLite/Drizzle might truncate ms or handle dates differently in test env
      expect(new Date(fetchedUser!.createdAt).getTime()).toBeLessThanOrEqual(
        createdUser.createdAt.getTime(),
      );
      expect(new Date(fetchedUser!.updatedAt).getTime()).toBeLessThanOrEqual(
        createdUser.updatedAt.getTime(),
      );
    });

    it("should update a user", async () => {
      const user = await db.createUser({
        email: "update@example.com",
        firstName: "Update",
        lastName: "Me",
        password: "password",
        role: "client",
      });

      const updated = await db.updateUser(user.id, { firstName: "Updated" });
      expect(updated?.firstName).toBe("Updated");

      const fetched = await db.getUserById(user.id);
      expect(fetched?.firstName).toBe("Updated");
    });

    it("should delete a user", async () => {
      const user = await db.createUser({
        email: "delete@example.com",
        firstName: "Delete",
        lastName: "Me",
        password: "password",
        role: "client",
      });

      const deleted = await db.deleteUser(user.id);
      expect(deleted).toBe(true);

      const fetched = await db.getUserById(user.id);
      expect(fetched).toBeNull();
    });
  });

  describe("Client Operations", () => {
    it("should create and retrieve a client", async () => {
      const user = await db.createUser({
        email: "client@example.com",
        firstName: "Client",
        lastName: "User",
        password: "password",
        role: "client",
      });

      const newClient = {
        userId: user.id,
        membershipType: "basic" as const,
        membershipStatus: "active" as const,
        joinDate: new Date(),
      };

      const createdClient = await db.createClient(newClient);
      expect(createdClient).toBeDefined();
      expect(createdClient.userId).toBe(user.id);

      const fetchedClient = await db.getClientById(createdClient.id);
      expect(fetchedClient).toBeDefined();
      expect(fetchedClient?.id).toBe(createdClient.id);
      expect(fetchedClient?.userId).toBe(createdClient.userId);
      expect(fetchedClient?.membershipType).toBe(createdClient.membershipType);
    });
  });

  describe("Fitness Profile Operations", () => {
    it("should create and retrieve a fitness profile", async () => {
      const user = await db.createUser({
        email: "profile@example.com",
        firstName: "Profile",
        lastName: "User",
        password: "password",
        role: "client",
      });

      const newProfile = {
        userId: user.id,
        height: 180,
        weight: 75,
        age: 30,
        gender: "male" as const,
        activityLevel: "moderately_active" as const,
        fitnessGoals: ["weight_loss"],
        exerciseHabits: "None",
        dietHabits: "None",
        medicalConditions: "None",
      };

      const createdProfile = await db.createFitnessProfile(newProfile as any);
      expect(createdProfile).toBeDefined();
      expect(createdProfile.userId).toBe(user.id);

      const fetchedProfile = await db.getFitnessProfileByUserId(user.id);
      expect(fetchedProfile).toBeDefined();
      expect(fetchedProfile?.userId).toBe(user.id);
      expect(fetchedProfile?.fitnessGoals).toEqual(
        expect.arrayContaining(["weight_loss"]),
      );
    });
  });

  describe("Attendance Operations", () => {
    it("should check in and check out", async () => {
      const user = await db.createUser({
        email: "attendance@example.com",
        firstName: "Attendance",
        lastName: "User",
        password: "password",
        role: "client",
      });

      const checkIn = await db.checkIn(user.id, "gym");
      expect(checkIn).toBeDefined();
      expect(checkIn.checkOutTime).toBeUndefined();

      const activeCheckIn = await db.getActiveCheckIn(user.id);
      expect(activeCheckIn).toBeDefined();
      expect(activeCheckIn?.id).toBe(checkIn.id);

      const checkOut = await db.checkOut(checkIn.id);
      expect(checkOut?.checkOutTime).toBeDefined();

      const activeCheckInAfter = await db.getActiveCheckIn(user.id);
      expect(activeCheckInAfter).toBeNull();
    });
  });

  describe("Batch Query Operations (Phase 9)", () => {
    describe("getClientsByUserIds", () => {
      it("should return clients for multiple users", async () => {
        // Create users
        const user1 = await db.createUser({
          email: "batch1@example.com",
          firstName: "Batch",
          lastName: "User1",
          password: "password",
          role: "client",
        });
        const user2 = await db.createUser({
          email: "batch2@example.com",
          firstName: "Batch",
          lastName: "User2",
          password: "password",
          role: "client",
        });
        const user3 = await db.createUser({
          email: "batch3@example.com",
          firstName: "Batch",
          lastName: "User3",
          password: "password",
          role: "client",
        });

        // Create clients for user1 and user2 only
        await db.createClient({
          userId: user1.id,
          membershipType: "basic",
          membershipStatus: "active",
          joinDate: new Date(),
        });
        await db.createClient({
          userId: user2.id,
          membershipType: "premium",
          membershipStatus: "active",
          joinDate: new Date(),
        });

        // Batch load clients
        const clients = await db.getClientsByUserIds([
          user1.id,
          user2.id,
          user3.id,
        ]);

        expect(clients).toHaveLength(2);
        expect(clients.map((c) => c.userId).sort()).toEqual(
          [user1.id, user2.id].sort(),
        );
        expect(clients.find((c) => c.userId === user3.id)).toBeUndefined();
      });

      it("should return empty array for empty input", async () => {
        const clients = await db.getClientsByUserIds([]);
        expect(clients).toEqual([]);
      });

      it("should return empty array when no clients exist for users", async () => {
        const clients = await db.getClientsByUserIds(["nonexistent-id"]);
        expect(clients).toEqual([]);
      });
    });

    describe("getAttendanceHistoriesByUserIds", () => {
      it("should return attendance records for multiple users", async () => {
        const user1 = await db.createUser({
          email: "attendance1@example.com",
          firstName: "Attendance",
          lastName: "User1",
          password: "password",
          role: "client",
        });
        const user2 = await db.createUser({
          email: "attendance2@example.com",
          firstName: "Attendance",
          lastName: "User2",
          password: "password",
          role: "client",
        });

        // Create attendance records
        await db.checkIn(user1.id, "gym");
        await db.checkIn(user1.id, "gym");
        await db.checkIn(user2.id, "gym");

        const attendances = await db.getAttendanceHistoriesByUserIds([
          user1.id,
          user2.id,
        ]);

        expect(attendances.length).toBeGreaterThanOrEqual(3);
        const user1Records = attendances.filter((a) => a.userId === user1.id);
        const user2Records = attendances.filter((a) => a.userId === user2.id);
        expect(user1Records.length).toBeGreaterThanOrEqual(2);
        expect(user2Records.length).toBeGreaterThanOrEqual(1);
      });

      it("should return records ordered by checkInTime DESC", async () => {
        const user = await db.createUser({
          email: "order@example.com",
          firstName: "Order",
          lastName: "Test",
          password: "password",
          role: "client",
        });

        // Create multiple check-ins with slight delays
        const checkIn1 = await db.checkIn(user.id, "gym");
        await new Promise((resolve) => setTimeout(resolve, 10));
        const checkIn2 = await db.checkIn(user.id, "gym");

        const attendances = await db.getAttendanceHistoriesByUserIds([user.id]);

        expect(attendances.length).toBeGreaterThanOrEqual(2);
        // Most recent should be first
        expect(
          new Date(attendances[0].checkInTime).getTime(),
        ).toBeGreaterThanOrEqual(
          new Date(attendances[1].checkInTime).getTime(),
        );
      });

      it("should return empty array for empty input", async () => {
        const attendances = await db.getAttendanceHistoriesByUserIds([]);
        expect(attendances).toEqual([]);
      });
    });

    describe("getActiveCheckInsByUserIds", () => {
      it("should return only active check-ins", async () => {
        const user1 = await db.createUser({
          email: "active1@example.com",
          firstName: "Active",
          lastName: "User1",
          password: "password",
          role: "client",
        });
        const user2 = await db.createUser({
          email: "active2@example.com",
          firstName: "Active",
          lastName: "User2",
          password: "password",
          role: "client",
        });

        // User1: check in and check out
        const checkIn1 = await db.checkIn(user1.id, "gym");
        await db.checkOut(checkIn1.id);

        // User2: check in only (active)
        const checkIn2 = await db.checkIn(user2.id, "gym");

        const activeCheckIns = await db.getActiveCheckInsByUserIds([
          user1.id,
          user2.id,
        ]);

        expect(activeCheckIns).toHaveLength(1);
        expect(activeCheckIns[0].userId).toBe(user2.id);
        expect(activeCheckIns[0].checkOutTime).toBeUndefined();
      });

      it("should return empty array when no active check-ins exist", async () => {
        const user = await db.createUser({
          email: "noactive@example.com",
          firstName: "NoActive",
          lastName: "User",
          password: "password",
          role: "client",
        });

        const checkIn = await db.checkIn(user.id, "gym");
        await db.checkOut(checkIn.id);

        const activeCheckIns = await db.getActiveCheckInsByUserIds([user.id]);
        expect(activeCheckIns).toEqual([]);
      });

      it("should return empty array for empty input", async () => {
        const activeCheckIns = await db.getActiveCheckInsByUserIds([]);
        expect(activeCheckIns).toEqual([]);
      });
    });

    describe("getRecommendationsByUserIds", () => {
      it("should return recommendations for multiple users", async () => {
        const user1 = await db.createUser({
          email: "rec1@example.com",
          firstName: "Rec",
          lastName: "User1",
          password: "password",
          role: "client",
        });
        const user2 = await db.createUser({
          email: "rec2@example.com",
          firstName: "Rec",
          lastName: "User2",
          password: "password",
          role: "client",
        });

        // Create recommendations
        // Create fitness profiles (required for recommendations)
        const profile1 = await db.createFitnessProfile({
          userId: user1.id,
          height: 180,
          weight: 75,
          age: 30,
          gender: "male",
          activityLevel: "moderately_active",
        } as any);

        const profile2 = await db.createFitnessProfile({
          userId: user2.id,
          height: 165,
          weight: 60,
          age: 25,
          gender: "female",
          activityLevel: "lightly_active",
        } as any);

        await db.createRecommendation({
          userId: user1.id,
          fitnessProfileId: profile1.id,
          recommendationText: "Test recommendation 1",
          activityPlan: "Activity plan 1",
          dietPlan: "Diet plan 1",
          status: "pending",
        } as any);
        await db.createRecommendation({
          userId: user2.id,
          fitnessProfileId: profile2.id,
          recommendationText: "Test recommendation 2",
          activityPlan: "Activity plan 2",
          dietPlan: "Diet plan 2",
          status: "pending",
        } as any);

        const recommendations = await db.getRecommendationsByUserIds([
          user1.id,
          user2.id,
        ]);

        expect(recommendations.length).toBeGreaterThanOrEqual(2);
        const user1Recs = recommendations.filter((r) => r.userId === user1.id);
        const user2Recs = recommendations.filter((r) => r.userId === user2.id);
        expect(user1Recs.length).toBeGreaterThanOrEqual(1);
        expect(user2Recs.length).toBeGreaterThanOrEqual(1);
      });

      it("should return empty array for empty input", async () => {
        const recommendations = await db.getRecommendationsByUserIds([]);
        expect(recommendations).toEqual([]);
      });
    });

    describe("getFitnessGoalsByUserIds", () => {
      it("should return fitness goals for multiple users", async () => {
        const user1 = await db.createUser({
          email: "goal1@example.com",
          firstName: "Goal",
          lastName: "User1",
          password: "password",
          role: "client",
        });
        const user2 = await db.createUser({
          email: "goal2@example.com",
          firstName: "Goal",
          lastName: "User2",
          password: "password",
          role: "client",
        });

        // Create fitness goals
        await db.createFitnessGoal({
          userId: user1.id,
          goalType: "weight_target",
          title: "Lose weight",
          targetValue: 10,
          currentValue: 0,
          unit: "kg",
          targetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
          status: "active",
        } as any);
        await db.createFitnessGoal({
          userId: user2.id,
          goalType: "strength_milestone",
          title: "Gain muscle",
          targetValue: 5,
          currentValue: 0,
          unit: "kg",
          targetDate: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000),
          status: "active",
        } as any);

        const goals = await db.getFitnessGoalsByUserIds([user1.id, user2.id]);

        expect(goals.length).toBeGreaterThanOrEqual(2);
        const user1Goals = goals.filter((g) => g.userId === user1.id);
        const user2Goals = goals.filter((g) => g.userId === user2.id);
        expect(user1Goals.length).toBeGreaterThanOrEqual(1);
        expect(user2Goals.length).toBeGreaterThanOrEqual(1);
      });

      it("should return empty array for empty input", async () => {
        const goals = await db.getFitnessGoalsByUserIds([]);
        expect(goals).toEqual([]);
      });
    });

    describe("Batch Query Performance", () => {
      it("should be more efficient than individual queries", async () => {
        // Create 10 users with clients
        const users = [];
        for (let i = 0; i < 10; i++) {
          const user = await db.createUser({
            email: `perf${i}@example.com`,
            firstName: "Perf",
            lastName: `User${i}`,
            password: "password",
            role: "client",
          });
          await db.createClient({
            userId: user.id,
            membershipType: "basic",
            membershipStatus: "active",
            joinDate: new Date(),
          });
          users.push(user);
        }

        const userIds = users.map((u) => u.id);

        // Batch query (should be 1 query)
        const batchStart = Date.now();
        const batchClients = await db.getClientsByUserIds(userIds);
        const batchTime = Date.now() - batchStart;

        // Individual queries (should be 10 queries)
        const individualStart = Date.now();
        const individualClients = [];
        for (const userId of userIds) {
          const client = await db.getClientByUserId(userId);
          if (client) individualClients.push(client);
        }
        const individualTime = Date.now() - individualStart;

        // Verify same results
        expect(batchClients.length).toBe(individualClients.length);
        expect(batchClients.length).toBe(10);

        // Batch should be faster (or at least not significantly slower)
        // In practice, batch is much faster, but in-memory DB might not show huge difference
        expect(batchTime).toBeLessThanOrEqual(individualTime * 2);
      });
    });
  });
});
