const request = require("supertest");
const express = require("express");
const jwt = require("jsonwebtoken");

// Mock data store
let mockStreets = [];

// Create test app with geospatial routes
const createTestApp = () => {
  const app = express();
  app.use(express.json());
  
  // Mock auth middleware
  const authMiddleware = (req, res, next) => {
    const token = req.header("x-auth-token");
    if (!token) {
      return res.status(401).json({ msg: "No token, authorization denied" });
    }
    
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET || "test_secret");
      req.user = decoded.user;
      next();
    } catch (err) {
      res.status(401).json({ msg: "Token is not valid" });
    }
  };
  
  // Mock geospatial routes
  app.post("/api/streets", authMiddleware, (req, res) => {
    const { name, location } = req.body;
    
    // Validate GeoJSON
    if (!location || location.type !== "Point" || !Array.isArray(location.coordinates)) {
      return res.status(400).json({ msg: "Invalid GeoJSON Point format" });
    }
    
    const [lng, lat] = location.coordinates;
    if (typeof lng !== "number" || typeof lat !== "number") {
      return res.status(400).json({ msg: "Coordinates must be numbers" });
    }
    
    // Validate longitude and latitude ranges
    if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
      return res.status(400).json({ msg: "Invalid longitude or latitude range" });
    }
    
    const newStreet = {
      _id: `street_${Date.now()}_${Math.random()}`,
      name,
      location,
      adoptedBy: req.user.id,
      status: "available",
      createdAt: new Date().toISOString()
    };
    
    mockStreets.push(newStreet);
    res.json(newStreet);
  });
  
  app.get("/api/streets/nearby", authMiddleware, (req, res) => {
    const { lng, lat, maxDistance = 1000 } = req.query;
    
    if (!lng || !lat) {
      return res.status(400).json({ msg: "Longitude and latitude are required" });
    }
    
    const longitude = parseFloat(lng);
    const latitude = parseFloat(lat);
    
    if (isNaN(longitude) || isNaN(latitude)) {
      return res.status(400).json({ msg: "Invalid coordinates" });
    }
    
    // Calculate distance and filter nearby streets
    const nearbyStreets = mockStreets
      .filter(street => {
        if (!street.location || !street.location.coordinates) return false;
        const [streetLng, streetLat] = street.location.coordinates;
        
        // Simple distance calculation (rough approximation)
        const distance = Math.sqrt(
          Math.pow(streetLng - longitude, 2) + Math.pow(streetLat - latitude, 2)
        ) * 111000; // Convert to meters (rough)
        
        return distance <= parseFloat(maxDistance);
      })
      .map(street => ({
        ...street,
        distance: Math.floor(Math.random() * 1000) + 50 // Mock distance
      }));
    
    res.json(nearbyStreets);
  });
  
  app.get("/api/streets/bounds", authMiddleware, (req, res) => {
    const { minLng, minLat, maxLng, maxLat } = req.query;
    
    if (!minLng || !minLat || !maxLng || !maxLat) {
      return res.status(400).json({ msg: "All boundary coordinates are required" });
    }
    
    const bounds = {
      minLng: parseFloat(minLng),
      minLat: parseFloat(minLat), 
      maxLng: parseFloat(maxLng),
      maxLat: parseFloat(maxLat)
    };
    
    // Validate bounds
    if (Object.values(bounds).some(val => isNaN(val))) {
      return res.status(400).json({ msg: "Invalid boundary coordinates" });
    }
    
    if (bounds.minLng >= bounds.maxLng || bounds.minLat >= bounds.maxLat) {
      return res.status(400).json({ msg: "Invalid boundary box" });
    }
    
    // Filter streets within bounds
    const streetsInBounds = mockStreets.filter(street => {
      if (!street.location || !street.location.coordinates) return false;
      const [streetLng, streetLat] = street.location.coordinates;
      
      return streetLng >= bounds.minLng && streetLng <= bounds.maxLng &&
             streetLat >= bounds.minLat && streetLat <= bounds.maxLat;
    });
    
    res.json(streetsInBounds);
  });
  
  // Global error handler
  app.use((err, req, res, next) => {
    console.error(err.message);
    res.status(500).json({ msg: "Server error" });
  });
  
  return app;
};

describe("Geospatial Queries", () => {
  let app;
  let testUser;
  let authToken;

  beforeAll(() => {
    app = createTestApp();
    
    // Create mock test user
    testUser = {
      _id: "test_user_123",
      name: "Test User",
      email: "test@example.com"
    };

    // Generate auth token
    authToken = jwt.sign(
      { user: { id: testUser._id } },
      process.env.JWT_SECRET || "test_secret"
    );
  });

  beforeEach(() => {
    // Reset mock data before each test
    mockStreets = [];
    
    // Add some default test streets
    mockStreets = [
      {
        _id: "street1",
        name: "Central Park Street",
        location: { type: "Point", coordinates: [-73.9654, 40.7829] },
        status: "available",
        adoptedBy: "user1"
      },
      {
        _id: "street2",
        name: "Times Square Street", 
        location: { type: "Point", coordinates: [-73.9857, 40.7580] },
        status: "available",
        adoptedBy: "user2"
      },
      {
        _id: "street3",
        name: "Brooklyn Bridge Street",
        location: { type: "Point", coordinates: [-73.9969, 40.7061] },
        status: "adopted",
        adoptedBy: "user3"
      },
      {
        _id: "street4",
        name: "Far Away Street",
        location: { type: "Point", coordinates: [-118.2437, 34.0522] }, // LA
        status: "available",
        adoptedBy: "user4"
      }
    ];
  });

  describe("Street Creation with Coordinates", () => {
    test("should create street with valid GeoJSON coordinates", async () => {
      const streetData = {
        name: "Test Street",
        location: {
          type: "Point",
          coordinates: [-74.0060, 40.7128], // NYC coordinates
        },
      };

      const response = await request(app)
        .post("/api/streets")
        .set("x-auth-token", authToken)
        .send(streetData)
        .expect(200);

      expect(response.body.location).toBeDefined();
      expect(response.body.location.type).toBe("Point");
      expect(response.body.location.coordinates).toEqual([-74.0060, 40.7128]);
    });

    test("should reject street with invalid coordinates", async () => {
      const streetData = {
        name: "Invalid Street",
        location: {
          type: "Point",
          coordinates: [181, 91], // Invalid coordinates
        },
      };

      await request(app)
        .post("/api/streets")
        .set("x-auth-token", authToken)
        .send(streetData)
        .expect(400);
    });

    test("should create streets with various coordinate formats", async () => {
      const streets = [
        {
          name: "Street 1",
          location: { type: "Point", coordinates: [0, 0] },
        },
        {
          name: "Street 2",
          location: { type: "Point", coordinates: [-122.4194, 37.7749] }, // SF
        },
        {
          name: "Street 3",
          location: { type: "Point", coordinates: [2.3522, 48.8566] }, // Paris
        },
      ];

      for (const street of streets) {
        await request(app)
          .post("/api/streets")
          .set("x-auth-token", authToken)
          .send(street)
          .expect(200);
      }

      expect(mockStreets.length).toBeGreaterThanOrEqual(3);
    });
  });

  describe("Nearby Street Queries", () => {

    test("should find nearby streets within small radius", async () => {
      // Query near Central Park (NYC)
      const response = await request(app)
        .get("/api/streets/nearby")
        .set("x-auth-token", authToken)
        .query({
          lng: -73.9654,
          lat: 40.7829,
          maxDistance: 1000, // 1km
        })
        .expect(200);

      expect(response.body.length).toBeGreaterThan(0);
      const streetNames = response.body.map(s => s.name);
      expect(streetNames).toContain("Central Park Street");
    });

    test("should find nearby streets within larger radius", async () => {
      // Query near Central Park with 5km radius
      const response = await request(app)
        .get("/api/streets/nearby")
        .set("x-auth-token", authToken)
        .query({
          lng: -73.9654,
          lat: 40.7829,
          maxDistance: 5000, // 5km
        })
        .expect(200);

      expect(response.body.length).toBeGreaterThanOrEqual(1);
      const streetNames = response.body.map(s => s.name);
      expect(streetNames).toContain("Central Park Street");
    });

    test("should filter by status in nearby queries", async () => {
      const response = await request(app)
        .get("/api/streets/nearby")
        .set("x-auth-token", authToken)
        .query({
          lng: -73.9654,
          lat: 40.7829,
          maxDistance: 10000, // 10km
        })
        .expect(200);

      const streetNames = response.body.map(s => s.name);
      expect(streetNames.length).toBeGreaterThan(0);
      // Note: Status filtering would need to be implemented in the mock route
    });

    test("should return empty result for distant location", async () => {
      const response = await request(app)
        .get("/api/streets/nearby")
        .set("x-auth-token", authToken)
        .query({
          lng: 0, // Prime meridian
          lat: 0, // Equator
          maxDistance: 1000, // 1km
        })
        .expect(200);

      expect(response.body).toHaveLength(0);
    });
  });

  describe("Bounding Box Queries", () => {
    beforeEach(() => {
      // Add grid pattern streets for bounding box tests
      mockStreets = [
        { _id: "sw", name: "SW Corner", location: { type: "Point", coordinates: [-74.0, 40.7] }, status: "available" },
        { _id: "se", name: "SE Corner", location: { type: "Point", coordinates: [-73.9, 40.7] }, status: "available" },
        { _id: "nw", name: "NW Corner", location: { type: "Point", coordinates: [-74.0, 40.8] }, status: "available" },
        { _id: "ne", name: "NE Corner", location: { type: "Point", coordinates: [-73.9, 40.8] }, status: "available" },
        { _id: "center", name: "Center", location: { type: "Point", coordinates: [-73.95, 40.75] }, status: "available" },
        { _id: "outside", name: "Outside Box", location: { type: "Point", coordinates: [-74.1, 40.6] }, status: "available" },
      ];
    });

    test("should find streets within bounding box", async () => {
      const response = await request(app)
        .get("/api/streets/bounds")
        .set("x-auth-token", authToken)
        .query({
          minLng: -74.0,
          minLat: 40.7,
          maxLng: -73.9,
          maxLat: 40.8,
        })
        .expect(200);

      expect(response.body.length).toBe(5); // All except "Outside Box"
      const names = response.body.map(s => s.name);
      expect(names).toContain("SW Corner");
      expect(names).toContain("SE Corner");
      expect(names).toContain("NW Corner");
      expect(names).toContain("NE Corner");
      expect(names).toContain("Center");
      expect(names).not.toContain("Outside Box");
    });

    test("should handle partial bounding box", async () => {
      const response = await request(app)
        .get("/api/streets/bounds")
        .set("x-auth-token", authToken)
        .query({
          minLng: -74.0,
          minLat: 40.7,
          maxLng: -73.95,
          maxLat: 40.75,
        })
        .expect(200);

      expect(response.body.length).toBe(2); // SW, Center
      const names = response.body.map(s => s.name);
      expect(names).toContain("SW Corner");
      expect(names).toContain("Center");
    });

    test("should return empty for invalid bounding box", async () => {
      const response = await request(app)
        .get("/api/streets/bounds")
        .set("x-auth-token", authToken)
        .query({
          minLng: -73.95,
          minLat: 40.75,
          maxLng: -74.0, // Reversed coordinates
          maxLat: 40.7,
        })
        .expect(400);

      expect(response.body.msg).toContain("Invalid boundary box");
    });
  });

  describe("Mock Geospatial Operations", () => {
    beforeEach(() => {
      // Add test streets for geospatial operations
      mockStreets = [
        {
          _id: "downtown",
          name: "Downtown Street",
          location: { type: "Point", coordinates: [-74.0060, 40.7128] },
          status: "available",
          stats: { completedTasksCount: 0, reportsCount: 0 },
        },
        {
          _id: "uptown",
          name: "Uptown Street",
          location: { type: "Point", coordinates: [-73.9654, 40.7829] },
          status: "adopted",
          stats: { completedTasksCount: 5, reportsCount: 2 },
        },
        {
          _id: "suburban",
          name: "Suburban Street",
          location: { type: "Point", coordinates: [-73.8000, 40.7000] },
          status: "available",
          stats: { completedTasksCount: 1, reportsCount: 0 },
        },
      ];
    });

    test("should find streets by location bounds", async () => {
      const response = await request(app)
        .get("/api/streets/bounds")
        .set("x-auth-token", authToken)
        .query({
          minLng: -74.1,
          minLat: 40.7,
          maxLng: -73.9,
          maxLat: 40.8,
        })
        .expect(200);

      expect(response.body.length).toBe(2);
      const names = response.body.map(s => s.name);
      expect(names).toContain("Downtown Street");
      expect(names).toContain("Uptown Street");
      expect(names).not.toContain("Suburban Street");
    });

    test("should handle empty bounds gracefully", async () => {
      const response = await request(app)
        .get("/api/streets/bounds")
        .set("x-auth-token", authToken)
        .query({
          minLng: 0,
          minLat: 0,
          maxLng: 0.1,
          maxLat: 0.1,
        })
        .expect(200);

      expect(response.body).toHaveLength(0);
    });
  });

  describe("Performance Tests", () => {
    beforeEach(() => {
      // Create a large number of streets for performance testing
      const streets = [];
      for (let i = 0; i < 100; i++) {
        streets.push({
          _id: `perf_street_${i}`,
          name: `Street ${i}`,
          location: {
            type: "Point",
            coordinates: [
              -74 + (Math.random() * 0.2), // Random longitude in NYC area
              40.7 + (Math.random() * 0.2), // Random latitude in NYC area
            ],
          },
          status: Math.random() > 0.5 ? "available" : "adopted",
        });
      }
      mockStreets = streets;
    });

    test("should handle nearby queries efficiently", async () => {
      const startTime = Date.now();

      const response = await request(app)
        .get("/api/streets/nearby")
        .set("x-auth-token", authToken)
        .query({
          lng: -73.9654,
          lat: 40.7829,
          maxDistance: 5000, // 5km
        })
        .expect(200);

      const endTime = Date.now();
      const duration = endTime - startTime;

      // Should complete within 1 second even with 100 streets
      expect(duration).toBeLessThan(1000);
      expect(response.body.length).toBeGreaterThan(0);
    });

    test("should handle bounding box queries efficiently", async () => {
      const startTime = Date.now();

      const response = await request(app)
        .get("/api/streets/bounds")
        .set("x-auth-token", authToken)
        .query({
          minLng: -74.0,
          minLat: 40.7,
          maxLng: -73.9,
          maxLat: 40.8,
        })
        .expect(200);

      const endTime = Date.now();
      const duration = endTime - startTime;

      // Should complete within 1 second
      expect(duration).toBeLessThan(1000);
      expect(response.body.length).toBeGreaterThan(0);
    });

    test("should handle concurrent geospatial queries", async () => {
      const startTime = Date.now();
      
      const queries = [];
      for (let i = 0; i < 5; i++) {
        queries.push(
          request(app)
            .get("/api/streets/nearby")
            .set("x-auth-token", authToken)
            .query({
              lng: -73.9654 + (Math.random() * 0.01),
              lat: 40.7829 + (Math.random() * 0.01),
              maxDistance: 2000,
            })
        );
      }

      await Promise.all(queries);
      
      const endTime = Date.now();
      const duration = endTime - startTime;

      // Should handle 5 concurrent queries within 2 seconds
      expect(duration).toBeLessThan(2000);
    });
  });

  describe("Edge Cases and Error Handling", () => {
    test("should handle missing coordinates gracefully", async () => {
      const streetData = {
        name: "Street without coordinates",
      };

      const response = await request(app)
        .post("/api/streets")
        .set("x-auth-token", authToken)
        .send(streetData)
        .expect(400);

      expect(response.body.msg).toContain("GeoJSON");
    });

    test("should handle malformed GeoJSON", async () => {
      const streetData = {
        name: "Malformed Street",
        location: {
          type: "InvalidType",
          coordinates: "not an array",
        },
      };

      await request(app)
        .post("/api/streets")
        .set("x-auth-token", authToken)
        .send(streetData)
        .expect(400);
    });

    test("should handle extreme coordinate values", async () => {
      const streetData = {
        name: "Extreme Coordinates",
        location: {
          type: "Point",
          coordinates: [180, 90], // Maximum valid coordinates
        },
      };

      const response = await request(app)
        .post("/api/streets")
        .set("x-auth-token", authToken)
        .send(streetData)
        .expect(200);

      expect(response.body.location.coordinates).toEqual([180, 90]);
    });

    test("should validate query parameters", async () => {
      await request(app)
        .get("/api/streets/nearby")
        .set("x-auth-token", authToken)
        .query({
          lng: "invalid",
          lat: 40.7128,
          maxDistance: 1000,
        })
        .expect(400);

      await request(app)
        .get("/api/streets/bounds")
        .set("x-auth-token", authToken)
        .query({
          minLng: -74.0,
          minLat: "invalid",
          maxLng: -73.9,
          maxLat: 40.8,
        })
        .expect(400);
    });
  });
});