// Integration tests for the Enhancement Two endpoints, run against a
// real (in-memory) MongoDB instance so the full Express -> Mongoose ->
// algorithm pipeline is exercised, including cursor pagination across
// multiple requests.
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
const express = require('express');
const request = require('supertest');
const fs = require('fs');
const path = require('path');

jest.setTimeout(120000);

let mongod;
let app;

beforeAll(async () => {
    mongod = await MongoMemoryServer.create();
    await mongoose.connect(mongod.getUri('travlr'));

    require('../models/travlr');
    const Model = mongoose.model('trips');
    const seed = JSON.parse(
        fs.readFileSync(path.join(__dirname, '../../data/trips.json'), 'utf8')
    );
    await Model.insertMany(seed);

    // Minimal Express app mounting only the API router (mirrors app.js).
    process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-secret';
    const apiRouter = require('../routes/index');
    app = express();
    app.use(express.json());
    app.use('/api', apiRouter);
});

afterAll(async () => {
    await mongoose.disconnect();
    if (mongod) {
        await mongod.stop();
    }
});

describe('GET /api/trips/search', () => {
    test('default request returns first page sorted by name with a cursor', async () => {
        const res = await request(app).get('/api/trips/search');
        expect(res.status).toBe(200);
        expect(res.body.results).toHaveLength(6);
        expect(res.body.totalMatches).toBe(27);
        expect(res.body.nextCursor).toBeTruthy();
        const names = res.body.results.map((t) => t.name);
        expect(names).toEqual([...names].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1));
    });

    test('cursor pagination walks all 27 trips with no duplicates or gaps', async () => {
        const seen = [];
        let cursor = null;
        for (let page = 0; page < 7; page += 1) {
            const url = cursor
                ? `/api/trips/search?limit=5&cursor=${encodeURIComponent(cursor)}`
                : '/api/trips/search?limit=5';
            const res = await request(app).get(url);
            expect(res.status).toBe(200);
            seen.push(...res.body.results.map((t) => t.code));
            cursor = res.body.nextCursor;
            if (!cursor) break;
        }
        expect(seen).toHaveLength(27);
        expect(new Set(seen).size).toBe(27);
    });

    test('sortBy=price ascending orders numerically, not lexically', async () => {
        const res = await request(app).get('/api/trips/search?sortBy=price&limit=50');
        const prices = res.body.results.map((t) => Number(t.perPerson));
        expect(prices[0]).toBe(549);
        for (let i = 1; i < prices.length; i += 1) {
            expect(prices[i]).toBeGreaterThanOrEqual(prices[i - 1]);
        }
    });

    test('search ranks name matches above description matches', async () => {
        const res = await request(app).get('/api/trips/search?search=reef');
        expect(res.status).toBe(200);
        expect(res.body.sortBy).toBe('relevance');
        expect(res.body.totalMatches).toBeGreaterThan(0);
        const first = res.body.results[0];
        expect(first.name.toLowerCase()).toContain('reef');
    });

    test('search with no matches returns an empty page cleanly', async () => {
        const res = await request(app).get('/api/trips/search?search=zzzznotathing');
        expect(res.status).toBe(200);
        expect(res.body.results).toEqual([]);
        expect(res.body.totalMatches).toBe(0);
        expect(res.body.nextCursor).toBeNull();
    });

    test('malformed cursor returns 400 JSON, not a crash', async () => {
        const res = await request(app).get('/api/trips/search?cursor=%25%25%25');
        expect(res.status).toBe(400);
        expect(res.body.message).toMatch(/cursor/i);
    });

    test('cursor replayed with different sort params returns 400', async () => {
        const first = await request(app).get('/api/trips/search?limit=3');
        const res = await request(app)
            .get(`/api/trips/search?sortBy=price&cursor=${encodeURIComponent(first.body.nextCursor)}`);
        expect(res.status).toBe(400);
    });

    test('invalid sortBy returns 400 with the allowed values', async () => {
        const res = await request(app).get('/api/trips/search?sortBy=hack');
        expect(res.status).toBe(400);
        expect(res.body.message).toMatch(/sortBy/);
    });

    test('limit is clamped to the server maximum', async () => {
        const res = await request(app).get('/api/trips/search?limit=99999');
        expect(res.status).toBe(200);
        expect(res.body.results.length).toBeLessThanOrEqual(50);
    });
});

describe('GET /api/trips/recommendations', () => {
    test('returns k similar trips, best first, excluding the seed', async () => {
        const res = await request(app).get('/api/trips/recommendations?basedOn=GALR210214&k=3');
        expect(res.status).toBe(200);
        expect(res.body.basedOn).toBe('GALR210214');
        expect(res.body.recommendations).toHaveLength(3);
        const codes = res.body.recommendations.map((t) => t.code);
        expect(codes).not.toContain('GALR210214');
        const sims = res.body.recommendations.map((t) => t.similarity);
        for (let i = 1; i < sims.length; i += 1) {
            expect(sims[i]).toBeLessThanOrEqual(sims[i - 1]);
        }
    });

    test('similar Emerald Bay trip ranks highly for the Gale Reef seed', async () => {
        const res = await request(app).get('/api/trips/recommendations?basedOn=GALR210214&k=5');
        const codes = res.body.recommendations.map((t) => t.code);
        expect(codes).toContain('EMRD220520'); // same resort tier, close price
    });

    test('category clusters emerge: a mountain seed recommends mountain trips', async () => {
        const res = await request(app).get('/api/trips/recommendations?basedOn=MTLG270118&k=3');
        const codes = res.body.recommendations.map((t) => t.code);
        const mountainCodes = codes.filter((c) => c.startsWith('MT'));
        expect(mountainCodes.length).toBeGreaterThanOrEqual(2);
    });

    test('category clusters emerge: a desert seed recommends desert trips', async () => {
        const res = await request(app).get('/api/trips/recommendations?basedOn=DSRT271004&k=3');
        const codes = res.body.recommendations.map((t) => t.code);
        const desertCodes = codes.filter((c) => c.startsWith('DS'));
        expect(desertCodes.length).toBeGreaterThanOrEqual(2);
    });

    test('unknown seed code returns 404', async () => {
        const res = await request(app).get('/api/trips/recommendations?basedOn=NOPE123');
        expect(res.status).toBe(404);
    });

    test('missing basedOn returns 400', async () => {
        const res = await request(app).get('/api/trips/recommendations');
        expect(res.status).toBe(400);
    });
});
