const request = require('supertest');

jest.mock('../src/lib/prisma', () => require('./__mocks__/prisma'));
const prisma = require('../src/lib/prisma');
const app = require('../src/app');

const EXP_ID = 'bbbbbbbb-0000-0000-0000-000000000001';
const ANIMAL = {
  id: 'cccccccc-0000-0000-0000-000000000001',
  experiment_id: EXP_ID,
  animal_id_string: 'M-001',
  animal_name: 'Whiskers',
  _count: { daily_statuses: 0 },
};

beforeEach(() => jest.clearAllMocks());

describe('GET /api/animals', () => {
  it('returns animals filtered by experimentId', async () => {
    prisma.animal.findMany.mockResolvedValue([ANIMAL]);
    const res = await request(app).get(`/api/animals?experimentId=${EXP_ID}`);
    expect(res.status).toBe(200);
    expect(res.body).toHaveLength(1);
    expect(res.body[0].animal_id_string).toBe('M-001');
  });

  it('returns 422 for invalid UUID experimentId', async () => {
    const res = await request(app).get('/api/animals?experimentId=not-a-uuid');
    expect(res.status).toBe(422);
  });
});

describe('GET /api/animals/:id', () => {
  it('returns animal with daily_statuses', async () => {
    prisma.animal.findUnique.mockResolvedValue({ ...ANIMAL, daily_statuses: [] });
    const res = await request(app).get(`/api/animals/${ANIMAL.id}`);
    expect(res.status).toBe(200);
    expect(res.body.animal_name).toBe('Whiskers');
  });

  it('returns 404 for unknown animal', async () => {
    prisma.animal.findUnique.mockResolvedValue(null);
    const res = await request(app).get('/api/animals/00000000-0000-0000-0000-000000000000');
    expect(res.status).toBe(404);
  });
});

describe('POST /api/animals', () => {
  it('creates animal with UUID and writes CREATE audit log', async () => {
    prisma.animal.create.mockResolvedValue(ANIMAL);
    prisma.auditLog.create.mockResolvedValue({});

    const res = await request(app)
      .post('/api/animals')
      .send({ experiment_id: EXP_ID, animal_id_string: 'M-001', animal_name: 'Whiskers' });

    expect(res.status).toBe(201);
    expect(res.body.id).toMatch(/^[0-9a-f-]{36}$/i);
    expect(prisma.auditLog.create).toHaveBeenCalled();
  });

  it('returns 422 when experiment_id is missing', async () => {
    const res = await request(app)
      .post('/api/animals')
      .send({ animal_id_string: 'M-001', animal_name: 'Whiskers' });
    expect(res.status).toBe(422);
    expect(res.body.details).toEqual(
      expect.arrayContaining([expect.objectContaining({ field: 'experiment_id' })])
    );
  });

  it('returns 422 when experiment_id is not a UUID', async () => {
    const res = await request(app)
      .post('/api/animals')
      .send({ experiment_id: 'bad-id', animal_id_string: 'M-001', animal_name: 'Whiskers' });
    expect(res.status).toBe(422);
  });

  it('returns 422 when animal_name is missing', async () => {
    const res = await request(app)
      .post('/api/animals')
      .send({ experiment_id: EXP_ID, animal_id_string: 'M-001' });
    expect(res.status).toBe(422);
  });
});

describe('PUT /api/animals/:id', () => {
  it('updates animal and writes UPDATE audit log', async () => {
    const updated = { ...ANIMAL, animal_name: 'Mr Whiskers' };
    prisma.animal.findUnique
      .mockResolvedValueOnce(ANIMAL)
      .mockResolvedValueOnce(updated);
    prisma.animal.update.mockResolvedValue(updated);
    prisma.auditLog.create.mockResolvedValue({});

    const res = await request(app)
      .put(`/api/animals/${ANIMAL.id}`)
      .send({ animal_name: 'Mr Whiskers' });

    expect(res.status).toBe(200);
    expect(res.body.animal_name).toBe('Mr Whiskers');
    expect(prisma.auditLog.create).toHaveBeenCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ action: 'UPDATE' }) })
    );
  });
});

describe('DELETE /api/animals/:id', () => {
  it('deletes animal and writes DELETE audit log', async () => {
    prisma.animal.findUnique.mockResolvedValue(ANIMAL);
    prisma.animal.delete.mockResolvedValue(ANIMAL);
    prisma.auditLog.create.mockResolvedValue({});

    const res = await request(app).delete(`/api/animals/${ANIMAL.id}`);
    expect(res.status).toBe(204);
    expect(prisma.auditLog.create).toHaveBeenCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ action: 'DELETE' }) })
    );
  });
});

describe('PATCH /api/animals/:id/csv-config', () => {
  const VALID_CONFIG = {
    buckets: [
      { name: 'Success', type: 'note', notes: ['s'] },
      { name: 'Failure', type: 'note', notes: ['f'] },
      { name: 'Other',   type: 'note', notes: [] },
      { name: 'Lick latency', type: 'numerical', notes: ['L25', 'L30'] },
    ],
  };

  it('updates csv_analysis_config and writes audit log', async () => {
    prisma.animal.findUnique.mockResolvedValue(ANIMAL);
    prisma.animal.update.mockResolvedValue({ ...ANIMAL, csv_analysis_config: VALID_CONFIG });
    prisma.auditLog.create.mockResolvedValue({});

    const res = await request(app)
      .patch(`/api/animals/${ANIMAL.id}/csv-config`)
      .send(VALID_CONFIG);

    expect(res.status).toBe(200);
    expect(res.body.csv_analysis_config).toEqual(VALID_CONFIG);
    expect(prisma.animal.update).toHaveBeenCalledWith({
      where: { id: ANIMAL.id },
      data: { csv_analysis_config: VALID_CONFIG },
    });
  });

  it('returns 400 when buckets is missing', async () => {
    const res = await request(app)
      .patch(`/api/animals/${ANIMAL.id}/csv-config`)
      .send({});
    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/buckets/);
  });

  it('returns 400 for empty bucket name', async () => {
    const bad = { buckets: [...VALID_CONFIG.buckets, { name: '  ', type: 'note', notes: [] }] };
    const res = await request(app)
      .patch(`/api/animals/${ANIMAL.id}/csv-config`)
      .send(bad);
    expect(res.status).toBe(400);
  });

  it('returns 400 for duplicate names', async () => {
    const bad = { buckets: [...VALID_CONFIG.buckets, { name: 'Success', type: 'note', notes: [] }] };
    const res = await request(app)
      .patch(`/api/animals/${ANIMAL.id}/csv-config`)
      .send(bad);
    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/duplicate/i);
  });

  it('returns 400 for invalid type', async () => {
    const bad = { buckets: [...VALID_CONFIG.buckets, { name: 'X', type: 'weird', notes: [] }] };
    const res = await request(app)
      .patch(`/api/animals/${ANIMAL.id}/csv-config`)
      .send(bad);
    expect(res.status).toBe(400);
  });

  it('returns 400 when missing default bucket', async () => {
    const bad = { buckets: VALID_CONFIG.buckets.filter((b) => b.name !== 'Success') };
    const res = await request(app)
      .patch(`/api/animals/${ANIMAL.id}/csv-config`)
      .send(bad);
    expect(res.status).toBe(400);
    expect(res.body.error).toMatch(/Success/);
  });
});
