const request = require("supertest");
const { app, server } = require("../../server");
const couchdbService = require("../../services/couchdbService");
const User = require("../../models/User");
const PointTransaction = require("../../models/PointTransaction");
const Badge = require("../../models/Badge");
const UserBadge = require("../../models/UserBadge");
const jwt = require("jsonwebtoken");
const { clearCache } = require("../../middleware/cache");

describe("Leaderboard Routes", () => {
  let testUsers = [];
  let authToken;
  let testUserId;

  beforeAll(async () => {
    await couchdbService.initialize();
  });

  beforeEach(async () => {
    // Clear cache before each test
    clearCache();

    // Create test users with varying points
    const userPromises = [];
    for (let i = 0; i < 10; i++) {
      const userData = {
        name: `Test User ${i}`,
        email: `testuser${i}@example.com`,
        password: "password123",
        points: (10 - i) * 100, // 1000, 900, 800, ..., 100
        adoptedStreets: [],
        completedTasks: [],
        stats: {
          streetsAdopted: i,
          tasksCompleted: i * 2,
          postsCreated: i,
          eventsParticipated: i,
          badgesEarned: 0
        }
      };
      userPromises.push(User.create(userData));
    }
    testUsers = await Promise.all(userPromises);

    // Set test user ID and create auth token
    testUserId = testUsers[0]._id;
    authToken = jwt.sign({ user: { id: testUserId } }, process.env.JWT_SECRET, {
      expiresIn: "1h"
    });

    // Create some point transactions for weekly/monthly leaderboards
    const now = new Date();
    const transactionPromises = [];

    // Create transactions for this week
    for (let i = 0; i < 5; i++) {
      const weeklyTransaction = {
        user: testUsers[i]._id,
        amount: (5 - i) * 50,
        transactionType: "earned",
        description: `Weekly test transaction ${i}`,
        balanceAfter: testUsers[i].points + (5 - i) * 50,
        createdAt: new Date(now.getTime() - i * 24 * 60 * 60 * 1000).toISOString()
      };
      transactionPromises.push(PointTransaction.create(weeklyTransaction));
    }

    // Create transactions for this month (but not this week)
    for (let i = 5; i < 8; i++) {
      const monthlyTransaction = {
        user: testUsers[i]._id,
        amount: (8 - i) * 30,
        transactionType: "earned",
        description: `Monthly test transaction ${i}`,
        balanceAfter: testUsers[i].points + (8 - i) * 30,
        createdAt: new Date(now.getTime() - (i + 5) * 24 * 60 * 60 * 1000).toISOString()
      };
      transactionPromises.push(PointTransaction.create(monthlyTransaction));
    }

    await Promise.all(transactionPromises);
  });

  afterEach(async () => {
    // Clean up test data
    const deletePromises = [];
    
    // Delete test users
    for (const user of testUsers) {
      if (user._id && user._rev) {
        deletePromises.push(
          couchdbService.deleteDocument(user._id, user._rev).catch(() => {})
        );
      }
    }

    // Delete test transactions
    const transactions = await couchdbService.find({
      selector: { type: "point_transaction" }
    });
    for (const transaction of transactions) {
      deletePromises.push(
        couchdbService.deleteDocument(transaction._id, transaction._rev).catch(() => {})
      );
    }

    await Promise.all(deletePromises);
    testUsers = [];
    clearCache();
  });

  afterAll(async () => {
    await new Promise((resolve) => {
      server.close(resolve);
    });
  });

  describe("GET /api/leaderboard/global", () => {
    it("should get global leaderboard with top users", async () => {
      const res = await request(app)
        .get("/api/leaderboard/global")
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(Array.isArray(res.body.data)).toBe(true);
      expect(res.body.data.length).toBeGreaterThan(0);
      expect(res.body.data.length).toBeLessThanOrEqual(100);

      // Check first user has highest points
      const firstUser = res.body.data[0];
      expect(firstUser).toHaveProperty("rank", 1);
      expect(firstUser).toHaveProperty("userId");
      expect(firstUser).toHaveProperty("username");
      expect(firstUser).toHaveProperty("points");
      expect(firstUser).toHaveProperty("streetsAdopted");
      expect(firstUser).toHaveProperty("tasksCompleted");
      expect(firstUser).toHaveProperty("badges");

      // Verify sorting by points descending
      for (let i = 0; i < res.body.data.length - 1; i++) {
        expect(res.body.data[i].points).toBeGreaterThanOrEqual(
          res.body.data[i + 1].points
        );
      }
    });

    it("should respect limit parameter", async () => {
      const res = await request(app)
        .get("/api/leaderboard/global?limit=5")
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(res.body.data.length).toBeLessThanOrEqual(5);
      expect(res.body.limit).toBe(5);
    });

    it("should respect offset parameter", async () => {
      const res1 = await request(app)
        .get("/api/leaderboard/global?limit=5")
        .expect(200);

      const res2 = await request(app)
        .get("/api/leaderboard/global?limit=5&offset=5")
        .expect(200);

      expect(res2.body.success).toBe(true);
      expect(res2.body.offset).toBe(5);
      
      // First user in second page should have lower points than last in first page
      if (res2.body.data.length > 0 && res1.body.data.length === 5) {
        expect(res2.body.data[0].points).toBeLessThanOrEqual(
          res1.body.data[4].points
        );
      }
    });

    it("should cache leaderboard results", async () => {
      const res1 = await request(app)
        .get("/api/leaderboard/global")
        .expect(200);

      const res2 = await request(app)
        .get("/api/leaderboard/global")
        .expect(200);

      expect(res1.body).toEqual(res2.body);
    });

    it("should limit maximum request to 500", async () => {
      const res = await request(app)
        .get("/api/leaderboard/global?limit=1000")
        .expect(200);

      expect(res.body.limit).toBe(500);
    });
  });

  describe("GET /api/leaderboard/weekly", () => {
    it("should get weekly leaderboard", async () => {
      const res = await request(app)
        .get("/api/leaderboard/weekly")
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(res.body.timeframe).toBe("week");
      expect(Array.isArray(res.body.data)).toBe(true);
    });

    it("should only include points from this week", async () => {
      const res = await request(app)
        .get("/api/leaderboard/weekly")
        .expect(200);

      expect(res.body.success).toBe(true);
      
      // Weekly leaderboard should have users with weekly transactions
      if (res.body.data.length > 0) {
        const firstUser = res.body.data[0];
        expect(firstUser).toHaveProperty("points");
        expect(firstUser).toHaveProperty("rank", 1);
      }
    });

    it("should respect limit and offset", async () => {
      const res = await request(app)
        .get("/api/leaderboard/weekly?limit=3&offset=1")
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(res.body.limit).toBe(3);
      expect(res.body.offset).toBe(1);
    });
  });

  describe("GET /api/leaderboard/monthly", () => {
    it("should get monthly leaderboard", async () => {
      const res = await request(app)
        .get("/api/leaderboard/monthly")
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(res.body.timeframe).toBe("month");
      expect(Array.isArray(res.body.data)).toBe(true);
    });

    it("should only include points from this month", async () => {
      const res = await request(app)
        .get("/api/leaderboard/monthly")
        .expect(200);

      expect(res.body.success).toBe(true);
      
      // Monthly leaderboard should have users with monthly transactions
      if (res.body.data.length > 0) {
        const firstUser = res.body.data[0];
        expect(firstUser).toHaveProperty("points");
        expect(firstUser).toHaveProperty("rank", 1);
      }
    });
  });

  describe("GET /api/leaderboard/friends", () => {
    it("should require authentication", async () => {
      const res = await request(app)
        .get("/api/leaderboard/friends")
        .expect(401);

      expect(res.body.success).toBe(false);
    });

    it("should get friends leaderboard with authentication", async () => {
      const res = await request(app)
        .get("/api/leaderboard/friends")
        .set("x-auth-token", authToken)
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(Array.isArray(res.body.data)).toBe(true);
    });

    it("should include user if no friends", async () => {
      const res = await request(app)
        .get("/api/leaderboard/friends")
        .set("x-auth-token", authToken)
        .expect(200);

      expect(res.body.success).toBe(true);
      // Should at least include the user themselves
      const userEntry = res.body.data.find(entry => entry.userId === testUserId);
      if (res.body.data.length > 0) {
        expect(userEntry || res.body.data.length === 1).toBeTruthy();
      }
    });
  });

  describe("GET /api/leaderboard/user/:userId", () => {
    it("should get user leaderboard position", async () => {
      const res = await request(app)
        .get(`/api/leaderboard/user/${testUserId}`)
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(res.body.data).toHaveProperty("rank");
      expect(res.body.data).toHaveProperty("userId", testUserId);
      expect(res.body.data).toHaveProperty("username");
      expect(res.body.data).toHaveProperty("points");
      expect(res.body.data).toHaveProperty("totalUsers");
      expect(res.body.data).toHaveProperty("percentile");
    });

    it("should support timeframe parameter", async () => {
      const res = await request(app)
        .get(`/api/leaderboard/user/${testUserId}?timeframe=week`)
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(res.body.data).toHaveProperty("rank");
    });

    it("should return 404 for non-existent user", async () => {
      const res = await request(app)
        .get("/api/leaderboard/user/nonexistent_user_id")
        .expect(404);

      expect(res.body.success).toBe(false);
    });
  });

  describe("GET /api/leaderboard/stats", () => {
    it("should get leaderboard statistics", async () => {
      const res = await request(app)
        .get("/api/leaderboard/stats")
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(res.body.data).toHaveProperty("totalUsers");
      expect(res.body.data).toHaveProperty("totalPoints");
      expect(res.body.data).toHaveProperty("avgPoints");
      expect(res.body.data).toHaveProperty("maxPoints");
      expect(res.body.data).toHaveProperty("minPoints");
      expect(res.body.data).toHaveProperty("weeklyStats");
      
      expect(res.body.data.weeklyStats).toHaveProperty("totalPoints");
      expect(res.body.data.weeklyStats).toHaveProperty("activeUsers");
      expect(res.body.data.weeklyStats).toHaveProperty("transactions");
    });

    it("should cache statistics", async () => {
      const res1 = await request(app)
        .get("/api/leaderboard/stats")
        .expect(200);

      const res2 = await request(app)
        .get("/api/leaderboard/stats")
        .expect(200);

      expect(res1.body).toEqual(res2.body);
    });
  });

  describe("Edge Cases", () => {
    it("should handle empty leaderboard gracefully", async () => {
      // Delete all users first
      const deletePromises = testUsers.map(user =>
        couchdbService.deleteDocument(user._id, user._rev).catch(() => {})
      );
      await Promise.all(deletePromises);
      clearCache();

      const res = await request(app)
        .get("/api/leaderboard/global")
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(res.body.data).toEqual([]);
    });

    it("should handle invalid limit gracefully", async () => {
      const res = await request(app)
        .get("/api/leaderboard/global?limit=invalid")
        .expect(200);

      expect(res.body.success).toBe(true);
      expect(typeof res.body.limit).toBe("number");
    });

    it("should handle negative offset gracefully", async () => {
      const res = await request(app)
        .get("/api/leaderboard/global?offset=-5")
        .expect(200);

      expect(res.body.success).toBe(true);
    });
  });
});
