/**
 * Trips API integration tests.
 * Covers TRIP-001 through TRIP-022.
 */
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
import request from 'supertest';
import type { Application } from 'express';

// ─────────────────────────────────────────────────────────────────────────────
// Step 1: Bare in-memory DB — schema applied in beforeAll after mocks register
// ─────────────────────────────────────────────────────────────────────────────
const { testDb, dbMock } = vi.hoisted(() => {
  const Database = require('better-sqlite3');
  const db = new Database(':memory:');
  db.exec('PRAGMA journal_mode = WAL');
  db.exec('PRAGMA foreign_keys = ON');
  db.exec('PRAGMA busy_timeout = 5000');

  const mock = {
    db,
    closeDb: () => {},
    reinitialize: () => {},
    getPlaceWithTags: (placeId: number) => {
      const place: any = db.prepare(`
        SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon
        FROM places p LEFT JOIN categories c ON p.category_id = c.id WHERE p.id = ?
      `).get(placeId);
      if (!place) return null;
      const tags = db.prepare(`SELECT t.* FROM tags t JOIN place_tags pt ON t.id = pt.tag_id WHERE pt.place_id = ?`).all(placeId);
      return { ...place, category: place.category_id ? { id: place.category_id, name: place.category_name, color: place.category_color, icon: place.category_icon } : null, tags };
    },
    canAccessTrip: (tripId: any, userId: number) =>
      db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
    isOwner: (tripId: any, userId: number) =>
      !!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
  };

  return { testDb: db, dbMock: mock };
});

vi.mock('../../src/db/database', () => dbMock);
vi.mock('../../src/config', () => ({
  JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
  ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
  updateJwtSecret: () => {},
}));

import { createApp } from '../../src/app';
import { createTables } from '../../src/db/schema';
import { runMigrations } from '../../src/db/migrations';
import { resetTestDb } from '../helpers/test-db';
import { createUser, createAdmin, createTrip, addTripMember, createPlace, createReservation, createTag, createDayAccommodation, createBudgetItem, createPackingItem, createDayNote, createDayAssignment } from '../helpers/factories';
import { authCookie } from '../helpers/auth';
import { loginAttempts, mfaAttempts } from '../../src/routes/auth';
import { invalidatePermissionsCache } from '../../src/services/permissions';

const app: Application = createApp();

beforeAll(() => { createTables(testDb); runMigrations(testDb); });
beforeEach(() => {
  resetTestDb(testDb);
  loginAttempts.clear();
  mfaAttempts.clear();
  invalidatePermissionsCache();
});
afterAll(() => { testDb.close(); });

// ─────────────────────────────────────────────────────────────────────────────
// Create trip (TRIP-001, TRIP-002, TRIP-003)
// ─────────────────────────────────────────────────────────────────────────────

describe('Create trip', () => {
  it('TRIP-001 — POST /api/trips with start_date/end_date returns 201 and auto-generates days', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .post('/api/trips')
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Paris Adventure', start_date: '2026-06-01', end_date: '2026-06-05' });

    expect(res.status).toBe(201);
    expect(res.body.trip).toBeDefined();
    expect(res.body.trip.title).toBe('Paris Adventure');

    // Verify days were generated (5 days: Jun 1–5)
    const days = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY date').all(res.body.trip.id) as any[];
    expect(days).toHaveLength(5);
    expect(days[0].date).toBe('2026-06-01');
    expect(days[4].date).toBe('2026-06-05');
  });

  it('TRIP-002 — POST /api/trips without dates returns 201 and creates 7 dateless placeholder days', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .post('/api/trips')
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Open-ended Trip' });

    expect(res.status).toBe(201);
    expect(res.body.trip).toBeDefined();
    expect(res.body.trip.start_date).toBeNull();
    expect(res.body.trip.end_date).toBeNull();

    // Should have 7 dateless placeholder days
    const days = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(res.body.trip.id) as any[];
    expect(days).toHaveLength(7);
    expect(days[0].date).toBeNull();
  });

  it('TRIP-002b — POST /api/trips with day_count creates correct number of dateless days', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .post('/api/trips')
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Custom Days Trip', day_count: 20 });

    expect(res.status).toBe(201);
    expect(res.body.trip.start_date).toBeNull();
    expect(res.body.trip.day_count).toBe(20);

    const days = testDb.prepare('SELECT * FROM days WHERE trip_id = ?').all(res.body.trip.id) as any[];
    expect(days).toHaveLength(20);
  });

  it('TRIP-001 — POST /api/trips requires a title, returns 400 without one', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .post('/api/trips')
      .set('Cookie', authCookie(user.id))
      .send({ description: 'No title here' });

    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/title/i);
  });

  it('TRIP-001 — POST /api/trips rejects end_date before start_date with 400', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .post('/api/trips')
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Bad Dates', start_date: '2026-06-10', end_date: '2026-06-05' });

    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/end date/i);
  });

  it('TRIP-003 — trip_create permission set to admin blocks regular user with 403', async () => {
    const { user } = createUser(testDb);

    // Restrict trip creation to admins only
    testDb.prepare("INSERT INTO app_settings (key, value) VALUES ('perm_trip_create', 'admin')").run();
    invalidatePermissionsCache();

    const res = await request(app)
      .post('/api/trips')
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Forbidden Trip' });

    expect(res.status).toBe(403);
    expect(res.body.error).toMatch(/permission/i);
  });

  it('TRIP-003 — trip_create permission set to admin allows admin user', async () => {
    const { user: admin } = createAdmin(testDb);

    testDb.prepare("INSERT INTO app_settings (key, value) VALUES ('perm_trip_create', 'admin')").run();
    invalidatePermissionsCache();

    const res = await request(app)
      .post('/api/trips')
      .set('Cookie', authCookie(admin.id))
      .send({ title: 'Admin Trip' });

    expect(res.status).toBe(201);
  });

  it('TRIP-001 — unauthenticated POST /api/trips returns 401', async () => {
    const res = await request(app).post('/api/trips').send({ title: 'No Auth' });
    expect(res.status).toBe(401);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// List trips (TRIP-004, TRIP-005)
// ─────────────────────────────────────────────────────────────────────────────

describe('List trips', () => {
  it('TRIP-004 — GET /api/trips returns own trips and member trips, not other users trips', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const { user: stranger } = createUser(testDb);

    const ownTrip = createTrip(testDb, owner.id, { title: "Owner's Trip" });
    const memberTrip = createTrip(testDb, stranger.id, { title: "Stranger's Trip (member)" });
    createTrip(testDb, stranger.id, { title: "Stranger's Private Trip" });

    // Add member to one of stranger's trips
    addTripMember(testDb, memberTrip.id, member.id);

    const ownerRes = await request(app)
      .get('/api/trips')
      .set('Cookie', authCookie(owner.id));

    expect(ownerRes.status).toBe(200);
    const ownerTripIds = ownerRes.body.trips.map((t: any) => t.id);
    expect(ownerTripIds).toContain(ownTrip.id);
    expect(ownerTripIds).not.toContain(memberTrip.id);

    const memberRes = await request(app)
      .get('/api/trips')
      .set('Cookie', authCookie(member.id));

    expect(memberRes.status).toBe(200);
    const memberTripIds = memberRes.body.trips.map((t: any) => t.id);
    expect(memberTripIds).toContain(memberTrip.id);
    expect(memberTripIds).not.toContain(ownTrip.id);
  });

  it('TRIP-005 — GET /api/trips excludes archived trips by default', async () => {
    const { user } = createUser(testDb);

    const activeTrip = createTrip(testDb, user.id, { title: 'Active Trip' });
    const archivedTrip = createTrip(testDb, user.id, { title: 'Archived Trip' });

    // Archive the second trip directly in the DB
    testDb.prepare('UPDATE trips SET is_archived = 1 WHERE id = ?').run(archivedTrip.id);

    const res = await request(app)
      .get('/api/trips')
      .set('Cookie', authCookie(user.id));

    expect(res.status).toBe(200);
    const tripIds = res.body.trips.map((t: any) => t.id);
    expect(tripIds).toContain(activeTrip.id);
    expect(tripIds).not.toContain(archivedTrip.id);
  });

  it('TRIP-005 — GET /api/trips?archived=1 returns only archived trips', async () => {
    const { user } = createUser(testDb);

    const activeTrip = createTrip(testDb, user.id, { title: 'Active Trip' });
    const archivedTrip = createTrip(testDb, user.id, { title: 'Archived Trip' });

    testDb.prepare('UPDATE trips SET is_archived = 1 WHERE id = ?').run(archivedTrip.id);

    const res = await request(app)
      .get('/api/trips?archived=1')
      .set('Cookie', authCookie(user.id));

    expect(res.status).toBe(200);
    const tripIds = res.body.trips.map((t: any) => t.id);
    expect(tripIds).toContain(archivedTrip.id);
    expect(tripIds).not.toContain(activeTrip.id);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Get trip (TRIP-006, TRIP-007, TRIP-016, TRIP-017)
// ─────────────────────────────────────────────────────────────────────────────

describe('Get trip', () => {
  it('TRIP-006 — GET /api/trips/:id for own trip returns 200 with full trip object', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'My Trip', description: 'A lovely trip' });

    const res = await request(app)
      .get(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id));

    expect(res.status).toBe(200);
    expect(res.body.trip).toBeDefined();
    expect(res.body.trip.id).toBe(trip.id);
    expect(res.body.trip.title).toBe('My Trip');
    expect(res.body.trip.is_owner).toBe(1);
  });

  it('TRIP-007 — GET /api/trips/:id for another users trip returns 404', async () => {
    const { user: owner } = createUser(testDb);
    const { user: other } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: "Owner's Trip" });

    const res = await request(app)
      .get(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(other.id));

    expect(res.status).toBe(404);
    expect(res.body.error).toMatch(/not found/i);
  });


  it('TRIP-017 — Member can access trip → 200', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Shared Trip' });
    addTripMember(testDb, trip.id, member.id);

    const res = await request(app)
      .get(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(member.id));

    expect(res.status).toBe(200);
    expect(res.body.trip.id).toBe(trip.id);
    expect(res.body.trip.is_owner).toBe(0);
  });

  it('TRIP-006 — GET /api/trips/:id for non-existent trip returns 404', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .get('/api/trips/999999')
      .set('Cookie', authCookie(user.id));

    expect(res.status).toBe(404);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Update trip (TRIP-008, TRIP-009, TRIP-010)
// ─────────────────────────────────────────────────────────────────────────────

describe('Update trip', () => {
  it('TRIP-008 — PUT /api/trips/:id updates title and description for owner → 200', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'Original Title' });

    const res = await request(app)
      .put(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Updated Title', description: 'New description' });

    expect(res.status).toBe(200);
    expect(res.body.trip.title).toBe('Updated Title');
    expect(res.body.trip.description).toBe('New description');
  });

  it('TRIP-009 — Archive trip (PUT with is_archived:true) removes it from normal list', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'To Archive' });

    const archiveRes = await request(app)
      .put(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id))
      .send({ is_archived: true });

    expect(archiveRes.status).toBe(200);
    expect(archiveRes.body.trip.is_archived).toBe(1);

    // Should not appear in the normal list
    const listRes = await request(app)
      .get('/api/trips')
      .set('Cookie', authCookie(user.id));

    const tripIds = listRes.body.trips.map((t: any) => t.id);
    expect(tripIds).not.toContain(trip.id);
  });

  it('TRIP-009 — Unarchive trip reappears in normal list', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'Archived Trip' });

    // Archive it first
    testDb.prepare('UPDATE trips SET is_archived = 1 WHERE id = ?').run(trip.id);

    // Unarchive via API
    const unarchiveRes = await request(app)
      .put(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id))
      .send({ is_archived: false });

    expect(unarchiveRes.status).toBe(200);
    expect(unarchiveRes.body.trip.is_archived).toBe(0);

    // Should appear in the normal list again
    const listRes = await request(app)
      .get('/api/trips')
      .set('Cookie', authCookie(user.id));

    const tripIds = listRes.body.trips.map((t: any) => t.id);
    expect(tripIds).toContain(trip.id);
  });

  it('TRIP-010 — Archive by trip member is denied when trip_archive is set to trip_owner', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Members Trip' });
    addTripMember(testDb, trip.id, member.id);

    // Restrict archiving to trip_owner only (this is actually the default, but set explicitly)
    testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('perm_trip_archive', 'trip_owner')").run();
    invalidatePermissionsCache();

    const res = await request(app)
      .put(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(member.id))
      .send({ is_archived: true });

    expect(res.status).toBe(403);
    expect(res.body.error).toMatch(/permission/i);
  });

  it('TRIP-008 — Member cannot edit trip title when trip_edit is set to trip_owner', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Original' });
    addTripMember(testDb, trip.id, member.id);

    // Default trip_edit is trip_owner — members should be blocked
    testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('perm_trip_edit', 'trip_owner')").run();
    invalidatePermissionsCache();

    const res = await request(app)
      .put(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(member.id))
      .send({ title: 'Hacked Title' });

    expect(res.status).toBe(403);
  });

  it('TRIP-008 — PUT /api/trips/:id returns 404 for non-existent trip', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .put('/api/trips/999999')
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Ghost Update' });

    expect(res.status).toBe(404);
  });

  it('TRIP-023 — Shifting trip date range preserves day assignments positionally', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { start_date: '2026-08-01', end_date: '2026-08-05' });

    const days = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string }[];
    expect(days).toHaveLength(5);

    const place = createPlace(testDb, trip.id);
    const assignment = createDayAssignment(testDb, days[0].id, place.id);
    const note = createDayNote(testDb, days[1].id, trip.id, { text: 'pack sunscreen' });

    // Shift forward 10 days (zero overlap with original range)
    const res = await request(app)
      .put(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id))
      .send({ start_date: '2026-08-11', end_date: '2026-08-15' });

    expect(res.status).toBe(200);

    const daysAfter = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string | null }[];
    expect(daysAfter).toHaveLength(5);
    expect(daysAfter.map(d => d.date)).toEqual(['2026-08-11', '2026-08-12', '2026-08-13', '2026-08-14', '2026-08-15']);

    const assignmentsAfter = testDb.prepare('SELECT * FROM day_assignments WHERE id = ?').get(assignment.id) as { day_id: number } | undefined;
    expect(assignmentsAfter).toBeDefined();
    expect(assignmentsAfter!.day_id).toBe(daysAfter[0].id);

    const notesAfter = testDb.prepare('SELECT * FROM day_notes WHERE id = ?').get(note.id) as { day_id: number } | undefined;
    expect(notesAfter).toBeDefined();
    expect(notesAfter!.day_id).toBe(daysAfter[1].id);
  });

  it('TRIP-024 — Shrinking trip date range deletes overflow days and their content', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-05' });

    const days = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number }[];
    const place = createPlace(testDb, trip.id);
    const a4 = createDayAssignment(testDb, days[3].id, place.id);
    const a5 = createDayAssignment(testDb, days[4].id, place.id);

    // Shrink from 5 to 3 days
    const res = await request(app)
      .put(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id))
      .send({ start_date: '2026-09-01', end_date: '2026-09-03' });

    expect(res.status).toBe(200);

    const daysAfter = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string | null }[];
    expect(daysAfter).toHaveLength(3);
    expect(daysAfter.every(d => d.date !== null)).toBe(true);

    // Overflow days and their assignments deleted
    const all = testDb.prepare('SELECT * FROM day_assignments WHERE id IN (?, ?)').all(a4.id, a5.id) as { id: number }[];
    expect(all).toHaveLength(0);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Delete trip (TRIP-018, TRIP-019, TRIP-022)
// ─────────────────────────────────────────────────────────────────────────────

describe('Delete trip', () => {
  it('TRIP-018 — DELETE /api/trips/:id by owner returns 200 and trip is no longer accessible', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'To Delete' });

    const deleteRes = await request(app)
      .delete(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id));

    expect(deleteRes.status).toBe(200);
    expect(deleteRes.body.success).toBe(true);

    // Trip should no longer be accessible
    const getRes = await request(app)
      .get(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id));

    expect(getRes.status).toBe(404);
  });

  it('TRIP-019 — Regular user cannot delete another users trip → 403', async () => {
    const { user: owner } = createUser(testDb);
    const { user: other } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: "Owner's Trip" });

    const res = await request(app)
      .delete(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(other.id));

    // getTripOwner finds the trip (it exists); checkPermission fails for non-members → 403
    expect(res.status).toBe(403);

    // Trip still exists
    const tripInDb = testDb.prepare('SELECT id FROM trips WHERE id = ?').get(trip.id);
    expect(tripInDb).toBeDefined();
  });

  it('TRIP-019 — Trip member cannot delete trip → 403', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Shared Trip' });
    addTripMember(testDb, trip.id, member.id);

    const res = await request(app)
      .delete(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(member.id));

    expect(res.status).toBe(403);
    expect(res.body.error).toMatch(/permission/i);
  });

  it('TRIP-022 — Trip with places and reservations can be deleted (cascade)', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'Trip With Data' });

    // Add associated data
    createPlace(testDb, trip.id, { name: 'Eiffel Tower' });
    createReservation(testDb, trip.id, { title: 'Hotel Booking', type: 'hotel' });

    const deleteRes = await request(app)
      .delete(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(user.id));

    expect(deleteRes.status).toBe(200);
    expect(deleteRes.body.success).toBe(true);

    // Verify cascade: places and reservations should be gone
    const places = testDb.prepare('SELECT id FROM places WHERE trip_id = ?').all(trip.id);
    expect(places).toHaveLength(0);

    const reservations = testDb.prepare('SELECT id FROM reservations WHERE trip_id = ?').all(trip.id);
    expect(reservations).toHaveLength(0);
  });

  it('TRIP-018 — Admin can delete another users trip', async () => {
    const { user: admin } = createAdmin(testDb);
    const { user: owner } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: "User's Trip" });

    const res = await request(app)
      .delete(`/api/trips/${trip.id}`)
      .set('Cookie', authCookie(admin.id));

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

  it('TRIP-018 — DELETE /api/trips/:id for non-existent trip returns 404', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .delete('/api/trips/999999')
      .set('Cookie', authCookie(user.id));

    expect(res.status).toBe(404);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Members (TRIP-013, TRIP-014, TRIP-015)
// ─────────────────────────────────────────────────────────────────────────────

describe('Trip members', () => {
  it('TRIP-015 — GET /api/trips/:id/members returns owner and members list', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Team Trip' });
    addTripMember(testDb, trip.id, member.id);

    const res = await request(app)
      .get(`/api/trips/${trip.id}/members`)
      .set('Cookie', authCookie(owner.id));

    expect(res.status).toBe(200);
    expect(res.body.owner).toBeDefined();
    expect(res.body.owner.id).toBe(owner.id);
    expect(Array.isArray(res.body.members)).toBe(true);
    expect(res.body.members.some((m: any) => m.id === member.id)).toBe(true);
    expect(res.body.current_user_id).toBe(owner.id);
  });

  it('TRIP-013 — POST /api/trips/:id/members adds a member by email → 201', async () => {
    const { user: owner } = createUser(testDb);
    const { user: invitee } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Team Trip' });

    const res = await request(app)
      .post(`/api/trips/${trip.id}/members`)
      .set('Cookie', authCookie(owner.id))
      .send({ identifier: invitee.email });

    expect(res.status).toBe(201);
    expect(res.body.member).toBeDefined();
    expect(res.body.member.email).toBe(invitee.email);
    expect(res.body.member.role).toBe('member');

    // Verify in DB
    const dbEntry = testDb.prepare('SELECT * FROM trip_members WHERE trip_id = ? AND user_id = ?').get(trip.id, invitee.id);
    expect(dbEntry).toBeDefined();
  });

  it('TRIP-013 — POST /api/trips/:id/members adds a member by username → 201', async () => {
    const { user: owner } = createUser(testDb);
    const { user: invitee } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Team Trip' });

    const res = await request(app)
      .post(`/api/trips/${trip.id}/members`)
      .set('Cookie', authCookie(owner.id))
      .send({ identifier: invitee.username });

    expect(res.status).toBe(201);
    expect(res.body.member.id).toBe(invitee.id);
  });

  it('TRIP-013 — Adding a non-existent user returns 404', async () => {
    const { user: owner } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Team Trip' });

    const res = await request(app)
      .post(`/api/trips/${trip.id}/members`)
      .set('Cookie', authCookie(owner.id))
      .send({ identifier: 'nobody@nowhere.example.com' });

    expect(res.status).toBe(404);
    expect(res.body.error).toMatch(/user not found/i);
  });

  it('TRIP-013 — Adding a user who is already a member returns 400', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Team Trip' });
    addTripMember(testDb, trip.id, member.id);

    const res = await request(app)
      .post(`/api/trips/${trip.id}/members`)
      .set('Cookie', authCookie(owner.id))
      .send({ identifier: member.email });

    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/already/i);
  });

  it('TRIP-013 — Adding a member by whitespace-padded username resolves correctly → 201', async () => {
    const { user: owner } = createUser(testDb);
    const { user: invitee } = createUser(testDb, { username: 'paddeduser' });
    const trip = createTrip(testDb, owner.id, { title: 'Padded Trip' });

    const res = await request(app)
      .post(`/api/trips/${trip.id}/members`)
      .set('Cookie', authCookie(owner.id))
      .send({ identifier: '  paddeduser  ' });

    expect(res.status).toBe(201);
    expect(res.body.member.id).toBe(invitee.id);
  });

  it('TRIP-014 — DELETE /api/trips/:id/members/:userId removes a member → 200', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Team Trip' });
    addTripMember(testDb, trip.id, member.id);

    const res = await request(app)
      .delete(`/api/trips/${trip.id}/members/${member.id}`)
      .set('Cookie', authCookie(owner.id));

    expect(res.status).toBe(200);
    expect(res.body.success).toBe(true);

    // Verify removal in DB
    const dbEntry = testDb.prepare('SELECT * FROM trip_members WHERE trip_id = ? AND user_id = ?').get(trip.id, member.id);
    expect(dbEntry).toBeUndefined();
  });

  it('TRIP-014 — Member can remove themselves from a trip → 200', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Team Trip' });
    addTripMember(testDb, trip.id, member.id);

    const res = await request(app)
      .delete(`/api/trips/${trip.id}/members/${member.id}`)
      .set('Cookie', authCookie(member.id));

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

  it('TRIP-013 — Non-owner member cannot add other members when member_manage is trip_owner', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const { user: invitee } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Team Trip' });
    addTripMember(testDb, trip.id, member.id);

    // Restrict member management to trip_owner (default)
    testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('perm_member_manage', 'trip_owner')").run();
    invalidatePermissionsCache();

    const res = await request(app)
      .post(`/api/trips/${trip.id}/members`)
      .set('Cookie', authCookie(member.id))
      .send({ identifier: invitee.email });

    expect(res.status).toBe(403);
    expect(res.body.error).toMatch(/permission/i);
  });

  it('TRIP-015 — Non-member cannot list trip members → 404', async () => {
    const { user: owner } = createUser(testDb);
    const { user: stranger } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Private Trip' });

    const res = await request(app)
      .get(`/api/trips/${trip.id}/members`)
      .set('Cookie', authCookie(stranger.id));

    expect(res.status).toBe(404);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Copy trip (TRIP-023, TRIP-024)
// ─────────────────────────────────────────────────────────────────────────────

describe('Copy trip', () => {
  it('TRIP-023 — POST /api/trips/:id/copy creates a duplicate trip with 201', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'Original Trip', description: 'Desc' });

    const res = await request(app)
      .post(`/api/trips/${trip.id}/copy`)
      .set('Cookie', authCookie(user.id))
      .send({});

    expect(res.status).toBe(201);
    expect(res.body.trip).toBeDefined();
    expect(res.body.trip.id).not.toBe(trip.id);
    expect(res.body.trip.title).toBe('Original Trip');
  });

  it('TRIP-023 — copy accepts a custom title for the new trip', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'Source' });

    const res = await request(app)
      .post(`/api/trips/${trip.id}/copy`)
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Custom Copy' });

    expect(res.status).toBe(201);
    expect(res.body.trip.title).toBe('Custom Copy');
  });

  it('TRIP-023 — copied trip belongs to the requesting user', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Shared Trip' });
    addTripMember(testDb, trip.id, member.id);

    const res = await request(app)
      .post(`/api/trips/${trip.id}/copy`)
      .set('Cookie', authCookie(member.id))
      .send({});

    expect(res.status).toBe(201);
    const newTrip = testDb.prepare('SELECT * FROM trips WHERE id = ?').get(res.body.trip.id) as any;
    expect(newTrip.user_id).toBe(member.id);
  });

  it('TRIP-024 — non-member cannot copy a trip → 404', async () => {
    const { user: owner } = createUser(testDb);
    const { user: stranger } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Private Trip' });

    const res = await request(app)
      .post(`/api/trips/${trip.id}/copy`)
      .set('Cookie', authCookie(stranger.id))
      .send({});

    expect(res.status).toBe(404);
  });

  it('TRIP-024 — copy of non-existent trip returns 404', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .post('/api/trips/999999/copy')
      .set('Cookie', authCookie(user.id))
      .send({});

    expect(res.status).toBe(404);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// ICS export (TRIP-025)
// ─────────────────────────────────────────────────────────────────────────────

describe('ICS export', () => {
  it('TRIP-025 — GET /api/trips/:id/export.ics returns text/calendar content', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'Calendar Trip' });

    const res = await request(app)
      .get(`/api/trips/${trip.id}/export.ics`)
      .set('Cookie', authCookie(user.id));

    expect(res.status).toBe(200);
    expect(res.headers['content-type']).toMatch(/text\/calendar/);
    expect(res.text).toContain('BEGIN:VCALENDAR');
    expect(res.text).toContain('END:VCALENDAR');
  });

  it('TRIP-025 — non-member cannot export ICS → 404', async () => {
    const { user: owner } = createUser(testDb);
    const { user: stranger } = createUser(testDb);
    const trip = createTrip(testDb, owner.id, { title: 'Private Trip' });

    const res = await request(app)
      .get(`/api/trips/${trip.id}/export.ics`)
      .set('Cookie', authCookie(stranger.id));

    expect(res.status).toBe(404);
  });

  it('TRIP-025 — unauthenticated export returns 401', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'Trip' });

    const res = await request(app).get(`/api/trips/${trip.id}/export.ics`);
    expect(res.status).toBe(401);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Copy trip with full data (covers loop bodies in the copy transaction)
// ─────────────────────────────────────────────────────────────────────────────

describe('Copy trip with data', () => {
  it('TRIP-026 — copy preserves days, places, tags, assignments, accommodations, reservations, budget, packing, notes', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, {
      title: 'Data-Rich Trip',
      start_date: '2025-09-01',
      end_date: '2025-09-03',
    });

    const days = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as any[];
    expect(days.length).toBe(3);

    // Place with a tag
    const place = createPlace(testDb, trip.id, { name: 'Tower Bridge' });
    const tag = createTag(testDb, user.id, { name: 'Landmark' });
    testDb.prepare('INSERT INTO place_tags (place_id, tag_id) VALUES (?, ?)').run(place.id, tag.id);

    // Day assignment
    testDb.prepare(
      'INSERT INTO day_assignments (day_id, place_id, order_index, notes) VALUES (?, ?, 0, ?)'
    ).run(days[0].id, place.id, 'Visit in morning');

    // Accommodation spanning days 0→1
    createDayAccommodation(testDb, trip.id, place.id, days[0].id, days[1].id);

    // Reservation on day 0
    createReservation(testDb, trip.id, { title: 'Flight Out', type: 'flight', day_id: days[0].id });

    // Budget item
    createBudgetItem(testDb, trip.id, { name: 'Flights', total_price: 400 });

    // Packing item
    createPackingItem(testDb, trip.id, { name: 'Toothbrush' });

    // Day note
    createDayNote(testDb, days[0].id, trip.id, { text: 'Pack early!' });

    const res = await request(app)
      .post(`/api/trips/${trip.id}/copy`)
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Data-Rich Trip (Copy)' });

    expect(res.status).toBe(201);
    const newId = res.body.trip.id;
    expect(newId).not.toBe(trip.id);

    // Days copied
    const newDays = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(newId) as any[];
    expect(newDays).toHaveLength(3);

    // Place copied
    const newPlaces = testDb.prepare('SELECT * FROM places WHERE trip_id = ?').all(newId) as any[];
    expect(newPlaces).toHaveLength(1);
    expect(newPlaces[0].name).toBe('Tower Bridge');

    // Place tag copied
    const newTags = testDb.prepare(
      'SELECT pt.* FROM place_tags pt JOIN places p ON p.id = pt.place_id WHERE p.trip_id = ?'
    ).all(newId) as any[];
    expect(newTags).toHaveLength(1);

    // Assignment copied
    const newAssignments = testDb.prepare(
      'SELECT da.* FROM day_assignments da JOIN days d ON d.id = da.day_id WHERE d.trip_id = ?'
    ).all(newId) as any[];
    expect(newAssignments).toHaveLength(1);

    // Accommodation copied
    const newAccom = testDb.prepare('SELECT * FROM day_accommodations WHERE trip_id = ?').all(newId) as any[];
    expect(newAccom).toHaveLength(1);

    // Reservation copied
    const newResv = testDb.prepare('SELECT * FROM reservations WHERE trip_id = ?').all(newId) as any[];
    expect(newResv).toHaveLength(1);

    // Budget copied
    const newBudget = testDb.prepare('SELECT * FROM budget_items WHERE trip_id = ?').all(newId) as any[];
    expect(newBudget).toHaveLength(1);

    // Packing copied (checked reset to 0)
    const newPacking = testDb.prepare('SELECT * FROM packing_items WHERE trip_id = ?').all(newId) as any[];
    expect(newPacking).toHaveLength(1);
    expect(newPacking[0].checked).toBe(0);

    // Day note copied
    const newNotes = testDb.prepare('SELECT * FROM day_notes WHERE trip_id = ?').all(newId) as any[];
    expect(newNotes).toHaveLength(1);
    expect(newNotes[0].text).toBe('Pack early!');
  });

  it('TRIP-027 — copy preserves todos (unchecked, unassigned) and budget category order', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { title: 'Todo Trip' });

    // Two todos: one checked and assigned — both should arrive unchecked and unassigned
    testDb.prepare(
      'INSERT INTO todo_items (trip_id, name, checked, category, sort_order, due_date, description, priority) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
    ).run(trip.id, 'Buy tickets', 0, 'Transport', 0, '2026-06-01', 'Check Ryanair', 1);
    testDb.prepare(
      'INSERT INTO todo_items (trip_id, name, checked, category, sort_order, assigned_user_id, priority) VALUES (?, ?, ?, ?, ?, ?, ?)'
    ).run(trip.id, 'Book hotel', 1, 'Accommodation', 1, user.id, 0);

    // Two budget category order rows
    const insOrder = testDb.prepare('INSERT INTO budget_category_order (trip_id, category, sort_order) VALUES (?, ?, ?)');
    insOrder.run(trip.id, 'Transport', 0);
    insOrder.run(trip.id, 'Accommodation', 1);

    const res = await request(app)
      .post(`/api/trips/${trip.id}/copy`)
      .set('Cookie', authCookie(user.id))
      .send({ title: 'Todo Trip (Copy)' });

    expect(res.status).toBe(201);
    const newId = res.body.trip.id;

    // Todos copied with checked reset and assigned_user_id nulled
    const newTodos = testDb.prepare('SELECT * FROM todo_items WHERE trip_id = ? ORDER BY sort_order').all(newId) as any[];
    expect(newTodos).toHaveLength(2);
    expect(newTodos[0].name).toBe('Buy tickets');
    expect(newTodos[0].category).toBe('Transport');
    expect(newTodos[0].checked).toBe(0);
    expect(newTodos[0].assigned_user_id).toBeNull();
    expect(newTodos[0].due_date).toBe('2026-06-01');
    expect(newTodos[0].description).toBe('Check Ryanair');
    expect(newTodos[0].priority).toBe(1);
    expect(newTodos[1].name).toBe('Book hotel');
    expect(newTodos[1].checked).toBe(0);
    expect(newTodos[1].assigned_user_id).toBeNull();

    // Budget category order copied
    const newOrder = testDb.prepare('SELECT category, sort_order FROM budget_category_order WHERE trip_id = ? ORDER BY sort_order').all(newId) as any[];
    expect(newOrder).toHaveLength(2);
    expect(newOrder[0]).toMatchObject({ category: 'Transport', sort_order: 0 });
    expect(newOrder[1]).toMatchObject({ category: 'Accommodation', sort_order: 1 });
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Bundle endpoint — GET /api/trips/:id/bundle
// ─────────────────────────────────────────────────────────────────────────────

describe('Trip bundle', () => {
  it('BUNDLE-001 — returns all sub-collections for owned trip', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id, { start_date: '2026-07-01', end_date: '2026-07-03' });

    const res = await request(app)
      .get(`/api/trips/${trip.id}/bundle`)
      .set('Cookie', authCookie(user.id));

    expect(res.status).toBe(200);
    expect(res.body.trip).toBeDefined();
    expect(res.body.trip.id).toBe(trip.id);
    expect(Array.isArray(res.body.days)).toBe(true);
    expect(res.body.days).toHaveLength(3);
    expect(Array.isArray(res.body.places)).toBe(true);
    expect(Array.isArray(res.body.packingItems)).toBe(true);
    expect(Array.isArray(res.body.todoItems)).toBe(true);
    expect(Array.isArray(res.body.budgetItems)).toBe(true);
    expect(Array.isArray(res.body.reservations)).toBe(true);
    expect(Array.isArray(res.body.files)).toBe(true);
  });

  it('BUNDLE-002 — returns 404 for trip that does not exist', async () => {
    const { user } = createUser(testDb);

    const res = await request(app)
      .get('/api/trips/999999/bundle')
      .set('Cookie', authCookie(user.id));

    expect(res.status).toBe(404);
  });

  it('BUNDLE-003 — returns 404 when user has no access to trip', async () => {
    const { user: owner } = createUser(testDb);
    const { user: other } = createUser(testDb);
    const trip = createTrip(testDb, owner.id);

    const res = await request(app)
      .get(`/api/trips/${trip.id}/bundle`)
      .set('Cookie', authCookie(other.id));

    expect(res.status).toBe(404);
  });

  it('BUNDLE-004 — members can fetch bundle', async () => {
    const { user: owner } = createUser(testDb);
    const { user: member } = createUser(testDb);
    const trip = createTrip(testDb, owner.id);
    testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, member.id);

    const res = await request(app)
      .get(`/api/trips/${trip.id}/bundle`)
      .set('Cookie', authCookie(member.id));

    expect(res.status).toBe(200);
    expect(res.body.trip.id).toBe(trip.id);
  });

  it('BUNDLE-005 — returns 401 when unauthenticated', async () => {
    const { user } = createUser(testDb);
    const trip = createTrip(testDb, user.id);

    const res = await request(app).get(`/api/trips/${trip.id}/bundle`);

    expect(res.status).toBe(401);
  });
});
