/**
 * Integration tests for authentication routes.
 *
 * Uses supertest against the createApp() factory (no real server).
 * HTTP calls to Emby are intercepted at the Node http/https layer using nock,
 * which works correctly with CJS require('axios') unlike vi.mock which only
 * intercepts ESM imports.
 *
 * Covers:
 *  - Input validation on /login (empty fields, overlong values)
 *  - Successful login flow (cookies set, CSRF token returned)
 *  - Failed login (wrong credentials → 401, no cookie set)
 *  - /me endpoint (authenticated vs unauthenticated)
 *  - /csrf token issuance
 *  - /logout (cookies cleared)
 */

import request from 'supertest';
import nock from 'nock';
import { createApp } from '../../server/app.js';

const EMBY_BASE = 'https://emby.test';

// Emby response fixtures
const EMBY_AUTH_BODY = {
  AccessToken: 'test-emby-token-abc123',
  User: { Id: 'user-id-001', Name: 'TestUser' }
};

const EMBY_USER_BODY = {
  Id: 'user-id-001',
  Name: 'TestUser',
  Policy: { IsAdministrator: false }
};

const EMBY_ADMIN_BODY = {
  Id: 'admin-id-001',
  Name: 'AdminUser',
  Policy: { IsAdministrator: true }
};

// Helper: intercept a successful Emby login + user-info sequence
function interceptSuccessfulLogin(userBody = EMBY_USER_BODY) {
  nock(EMBY_BASE)
    .post('/Users/authenticatebyname')
    .reply(200, EMBY_AUTH_BODY);
  nock(EMBY_BASE)
    .get(/\/Users\//)
    .reply(200, userBody);
}

afterEach(() => {
  nock.cleanAll(); // remove any pending interceptors between tests
});

describe('POST /api/auth/login', () => {
  // Each sub-describe gets a fresh app to avoid rate-limit state leaking
  // between the 'input validation' calls (which all fail and count toward
  // the 10-failure window) and the 'successful login' calls.
  let app;

  beforeEach(() => {
    process.env.EMBY_URL = 'https://emby.test';
    delete process.env.COOKIE_SECRET;
    // skipRateLimits avoids 429s from the login limiter when all
    // requests come from 127.0.0.1 in the test environment
    app = createApp({ skipRateLimits: true });
    vi.clearAllMocks();
  });

  afterEach(() => {
    delete process.env.EMBY_URL;
    delete process.env.COOKIE_SECRET;
  });

  describe('input validation', () => {
    it('rejects empty username', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: '', password: 'pass' });
      expect(res.status).toBe(400);
      expect(res.body.success).toBe(false);
    });

    it('rejects missing password', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'alice', password: '' });
      expect(res.status).toBe(400);
      expect(res.body.success).toBe(false);
    });

    it('rejects username over 128 chars', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'a'.repeat(129), password: 'pass' });
      expect(res.status).toBe(400);
    });

    it('rejects password over 256 chars', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'alice', password: 'p'.repeat(257) });
      expect(res.status).toBe(400);
    });

    it('rejects non-string username', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 123, password: 'pass' });
      expect(res.status).toBe(400);
    });
  });

  describe('successful login', () => {
    it('returns success:true with user info', async () => {
      interceptSuccessfulLogin();
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'TestUser', password: 'correct' });
      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
      expect(res.body.user.name).toBe('TestUser');
      expect(res.body.user.isAdmin).toBe(false);
    });

    it('sets emby_user cookie', async () => {
      interceptSuccessfulLogin();
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'TestUser', password: 'correct' });
      const cookies = res.headers['set-cookie'] || [];
      expect(cookies.some(c => c.startsWith('emby_user='))).toBe(true);
    });

    it('sets csrf_token cookie', async () => {
      interceptSuccessfulLogin();
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'TestUser', password: 'correct' });
      const cookies = res.headers['set-cookie'] || [];
      expect(cookies.some(c => c.startsWith('csrf_token='))).toBe(true);
    });

    it('returns csrfToken in response body', async () => {
      interceptSuccessfulLogin();
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'TestUser', password: 'correct' });
      expect(typeof res.body.csrfToken).toBe('string');
      expect(res.body.csrfToken.length).toBeGreaterThan(0);
    });

    it('session cookie has no maxAge when rememberMe is false', async () => {
      interceptSuccessfulLogin();
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'TestUser', password: 'correct', rememberMe: false });
      const cookies = res.headers['set-cookie'] || [];
      const sessionCookie = cookies.find(c => c.startsWith('emby_user='));
      // Session cookie must not persist across browser close
      expect(sessionCookie).toBeDefined();
      expect(sessionCookie).not.toContain('Max-Age');
    });

    it('sets 30-day maxAge when rememberMe is true', async () => {
      interceptSuccessfulLogin();
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'TestUser', password: 'correct', rememberMe: true });
      const cookies = res.headers['set-cookie'] || [];
      const sessionCookie = cookies.find(c => c.startsWith('emby_user='));
      expect(sessionCookie).toBeDefined();
      expect(sessionCookie).toContain('Max-Age');
    });

    it('marks isAdmin correctly for admin user', async () => {
      interceptSuccessfulLogin(EMBY_ADMIN_BODY);
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'AdminUser', password: 'correct' });
      expect(res.status).toBe(200);
      expect(res.body.user.isAdmin).toBe(true);
    });

    it('does not include AccessToken in response body', async () => {
      interceptSuccessfulLogin();
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'TestUser', password: 'correct' });
      // The Emby access token must never be sent to the client
      expect(JSON.stringify(res.body)).not.toContain('test-emby-token-abc123');
    });
  });

  describe('failed login', () => {
    it('returns 401 when Emby rejects credentials', async () => {
      nock(EMBY_BASE).post('/Users/authenticatebyname').reply(401, { error: 'Unauthorized' });
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'baduser', password: 'wrongpass' });
      expect(res.status).toBe(401);
      expect(res.body.success).toBe(false);
      // Must not expose internal error details
      expect(res.body.error).toBe('Invalid username or password');
    });

    it('does not set emby_user cookie on failure', async () => {
      nock(EMBY_BASE).post('/Users/authenticatebyname').reply(401, {});
      const res = await request(app)
        .post('/api/auth/login')
        .send({ username: 'baduser', password: 'wrongpass' });
      const cookies = res.headers['set-cookie'] || [];
      expect(cookies.some(c => c.startsWith('emby_user='))).toBe(false);
    });
  });
});

describe('GET /api/auth/me', () => {
  let app;

  beforeEach(() => {
    delete process.env.COOKIE_SECRET;
    app = createApp({ skipRateLimits: true });
  });

  it('returns authenticated:false when no cookie', async () => {
    const res = await request(app).get('/api/auth/me');
    expect(res.status).toBe(200);
    expect(res.body.authenticated).toBe(false);
  });

  it('returns authenticated:true with valid cookie', async () => {
    const payload = JSON.stringify({ id: 'u1', name: 'Alice', isAdmin: false });
    const res = await request(app)
      .get('/api/auth/me')
      .set('Cookie', `emby_user=${encodeURIComponent(payload)}`);
    expect(res.body.authenticated).toBe(true);
    expect(res.body.user.name).toBe('Alice');
  });
});

describe('GET /api/auth/csrf', () => {
  it('issues a csrf_token cookie and returns csrfToken in body', async () => {
    const app = createApp({ skipRateLimits: true });
    const res = await request(app).get('/api/auth/csrf');
    expect(res.status).toBe(200);
    expect(typeof res.body.csrfToken).toBe('string');
    expect(res.body.csrfToken.length).toBe(64); // 32 bytes hex
    const cookies = res.headers['set-cookie'] || [];
    expect(cookies.some(c => c.startsWith('csrf_token='))).toBe(true);
  });
});

describe('POST /api/auth/logout', () => {
  let app;

  beforeEach(() => {
    process.env.EMBY_URL = 'https://emby.test';
    delete process.env.COOKIE_SECRET;
    app = createApp({ skipRateLimits: true });
    vi.clearAllMocks();
  });

  afterEach(() => {
    delete process.env.EMBY_URL;
  });

  // NOTE: /api/auth/* is mounted BEFORE the verifyCsrf middleware in app.js,
  // so logout does not require a CSRF token by design. The session cookie's
  // sameSite:strict attribute provides equivalent CSRF protection for logout.
  it('succeeds without a CSRF token (sameSite:strict provides protection)', async () => {
    nock(EMBY_BASE).post('/Sessions/Logout').reply(200, {});
    const res = await request(app)
      .post('/api/auth/logout');
    expect(res.status).toBe(200);
    expect(res.body.success).toBe(true);
  });

  it('clears cookies and returns success when CSRF token is provided', async () => {
    const csrfRes = await request(app).get('/api/auth/csrf');
    const csrfToken = csrfRes.body.csrfToken;
    const csrfCookie = csrfRes.headers['set-cookie'].find(c => c.startsWith('csrf_token='));

    nock(EMBY_BASE).post('/Sessions/Logout').reply(200, {});

    const res = await request(app)
      .post('/api/auth/logout')
      .set('Cookie', csrfCookie)
      .set('X-CSRF-Token', csrfToken);

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

    // Cookies should be cleared (Set-Cookie header with empty value / Max-Age=0)
    const cookies = res.headers['set-cookie'] || [];
    expect(cookies.some(c => c.includes('emby_user=;') || c.includes('Max-Age=0'))).toBe(true);
  });
});
