import Fastify from 'fastify';
import { describe, expect, it, vi } from 'vitest';
import vibeSessionsRoutes, { VibeIdentityResolver } from './vibe-sessions.routes.js';
import { VibeSessionLifecycleError } from '../services/vibe-session-coordinator.service.js';

const SESSION_ID = '11111111-1111-4111-8111-111111111111';
const TRACK_ID = '22222222-2222-4222-8222-222222222222';
const USER_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';

function response() {
  return {
    sessionId: SESSION_ID, planVersion: 1, now: null, preview: [], state: {},
    replanned: false, replanReason: null,
    session: { id: SESSION_ID, status: 'active' },
  };
}

async function appWithCoordinator(identityResolver?: VibeIdentityResolver) {
  const coordinator = {
    start: vi.fn().mockResolvedValue(response()),
    getPlan: vi.fn().mockResolvedValue(response()),
    appendEvent: vi.fn().mockResolvedValue({ ...response(), event: { id: 'event-1' }, idempotent: false }),
    end: vi.fn().mockResolvedValue(response()),
    serveNext: vi.fn().mockResolvedValue(response()),
    advancePastUnplayable: vi.fn().mockResolvedValue(response()),
  } as any;
  const app = Fastify();
  await app.register(vibeSessionsRoutes, { coordinator, ...(identityResolver ? { identityResolver } : {}) });
  await app.ready();
  return { app, coordinator };
}

describe('durable Vibe session routes', () => {
  it('uses the caller identity so concurrent listeners receive separate sessions', async () => {
    const { app, coordinator } = await appWithCoordinator();
    const result = await app.inject({ method: 'POST', url: '/v2/vibe/sessions', headers: { 'x-user-id': USER_ID }, payload: {} });

    expect(result.statusCode).toBe(201);
    expect(coordinator.start).toHaveBeenCalledWith(USER_ID, expect.any(Object));
    await app.close();
  });

  it('uses the existing application default when no user header is provided', async () => {
    const { app, coordinator } = await appWithCoordinator();
    const result = await app.inject({
      method: 'POST', url: '/v2/vibe/sessions', payload: {},
    });
    expect(result.statusCode).toBe(201);
    expect(coordinator.start).toHaveBeenCalledWith('00000000-0000-0000-0000-000000000000', expect.any(Object));
    await app.close();
  });

  it('creates a session and validates event payloads before touching the coordinator', async () => {
    const { app, coordinator } = await appWithCoordinator(() => USER_ID);
    const created = await app.inject({
      method: 'POST', url: '/v2/vibe/sessions', headers: { 'x-user-id': 'spoofed' },
      payload: { seedTrackId: TRACK_ID },
    });
    const invalidEvent = await app.inject({
      method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/events`, headers: { 'x-user-id': 'spoofed' },
      payload: { type: 'definitely-not-an-event' },
    });

    expect(created.statusCode).toBe(201);
    expect(coordinator.start).toHaveBeenCalledWith(USER_ID, expect.objectContaining({ seedTrackId: TRACK_ID }));
    expect(invalidEvent.statusCode).toBe(400);
    expect(coordinator.appendEvent).not.toHaveBeenCalled();
    await app.close();
  });

  it('accepts bounded local calendar context and rejects malformed context', async () => {
    const { app, coordinator } = await appWithCoordinator();
    const valid = await app.inject({
      method: 'POST', url: '/v2/vibe/sessions',
      payload: { context: { localHour: 22, weekday: 5, month: 8, timeZone: 'Europe/Samara' } },
    });
    const invalid = await app.inject({
      method: 'POST', url: '/v2/vibe/sessions',
      payload: { context: { localHour: 24, weekday: 5, month: 8 } },
    });

    expect(valid.statusCode).toBe(201);
    expect(coordinator.start).toHaveBeenCalledWith(
      '00000000-0000-0000-0000-000000000000',
      expect.objectContaining({ context: { localHour: 22, weekday: 5, month: 8, timeZone: 'Europe/Samara' } }),
    );
    expect(invalid.statusCode).toBe(400);
    await app.close();
  });

  it('rejects server-only event types from the client event ledger', async () => {
    const { app, coordinator } = await appWithCoordinator();
    const result = await app.inject({
      method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/events`,
      payload: {
        type: 'track_served',
        trackId: TRACK_ID,
        payload: { planVersionId: '33333333-3333-4333-8333-333333333333', ordinal: 0 },
      },
    });

    expect(result.statusCode).toBe(400);
    expect(result.json()).toEqual({ error: 'type must be a supported client Vibe event type' });
    expect(coordinator.appendEvent).not.toHaveBeenCalled();
    await app.close();
  });

  it('returns a lifecycle conflict when an initial plan race ends or replaces the session', async () => {
    const { app, coordinator } = await appWithCoordinator();
    coordinator.start.mockRejectedValueOnce(
      new VibeSessionLifecycleError('Cannot publish a plan for ended Vibe session'),
    );

    const result = await app.inject({ method: 'POST', url: '/v2/vibe/sessions', payload: {} });

    expect(result.statusCode).toBe(409);
    expect(result.json()).toEqual({
      error: 'Cannot publish a plan for ended Vibe session',
      code: 'VIBE_SESSION_NOT_ACTIVE',
    });
    await app.close();
  });

  it('passes a requested plan revision and validated idempotent event through to the coordinator', async () => {
    const { app, coordinator } = await appWithCoordinator(() => USER_ID);
    const plan = await app.inject({
      method: 'GET', url: `/v2/vibe/sessions/${SESSION_ID}/plans?version=2`, headers: { 'x-user-id': 'spoofed' },
    });
    const event = await app.inject({
      method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/events`, headers: { 'x-user-id': 'spoofed' },
      payload: { eventId: '33333333-3333-4333-8333-333333333333', type: 'completed', trackId: TRACK_ID, positionMs: 5_000 },
    });

    expect(plan.statusCode).toBe(200);
    expect(coordinator.getPlan).toHaveBeenCalledWith(USER_ID, SESSION_ID, 2);
    expect(event.statusCode).toBe(200);
    expect(coordinator.appendEvent).toHaveBeenCalledWith(USER_ID, SESSION_ID, expect.objectContaining({ type: 'completed', positionMs: 5_000 }));
    await app.close();
  });

  it('validates occurredAt and exposes owned resume and advance operations', async () => {
    const { app, coordinator } = await appWithCoordinator(() => USER_ID);
    const resume = await app.inject({
      method: 'POST', url: '/v2/vibe/sessions', payload: { resumeSessionId: SESSION_ID },
    });
    const badTime = await app.inject({
      method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/events`,
      payload: { type: 'completed', occurredAt: 'not-a-date' },
    });
    const next = await app.inject({ method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/advance` });
    expect(resume.statusCode).toBe(201);
    expect(coordinator.start).toHaveBeenCalledWith(USER_ID, expect.objectContaining({ resumeSessionId: SESSION_ID }));
    expect(badTime.statusCode).toBe(400);
    expect(next.statusCode).toBe(200);
    expect(coordinator.serveNext).toHaveBeenCalledWith(USER_ID, SESSION_ID);
    await app.close();
  });

  it('passes a version-aware advance request through and rejects an invalid expected version', async () => {
    const { app, coordinator } = await appWithCoordinator(() => USER_ID);
    const valid = await app.inject({
      method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/advance`, payload: { expectedPlanVersion: 2 },
    });
    const invalid = await app.inject({
      method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/advance`, payload: { expectedPlanVersion: 0 },
    });

    expect(valid.statusCode).toBe(200);
    expect(coordinator.serveNext).toHaveBeenCalledWith(USER_ID, SESSION_ID, 2);
    expect(invalid.statusCode).toBe(400);
    expect(coordinator.serveNext).toHaveBeenCalledTimes(1);
    await app.close();
  });

  it('uses the explicit versioned advancement protocol for a served unplayable item', async () => {
    const { app, coordinator } = await appWithCoordinator(() => USER_ID);
    const result = await app.inject({
      method: 'POST', url: `/v2/vibe/sessions/${SESSION_ID}/advance`,
      payload: {
        expectedPlanVersion: 2,
        unplayable: {
          eventId: '33333333-3333-4333-8333-333333333333',
          planVersionId: '44444444-4444-4444-8444-444444444444',
          ordinal: 0,
          trackId: TRACK_ID,
        },
      },
    });

    expect(result.statusCode).toBe(200);
    expect(coordinator.advancePastUnplayable).toHaveBeenCalledWith(USER_ID, SESSION_ID, {
      expectedPlanVersion: 2,
      eventId: '33333333-3333-4333-8333-333333333333',
      planVersionId: '44444444-4444-4444-8444-444444444444',
      ordinal: 0,
      trackId: TRACK_ID,
    });
    expect(coordinator.serveNext).not.toHaveBeenCalled();
    await app.close();
  });
});
