import { test } from 'node:test';
import assert from 'node:assert/strict';
import { isTestDbConfigured, setupTestDb } from '../helpers/setup-db.js';
import express from 'express';
import session from 'express-session';
import authRouter from '../../src/routes/auth.js';
import { hashPassword } from '../../src/auth/passwords.js';
import { requireAuth, requireUiHeader } from '../../src/middleware/auth.js';

async function appWithAuth(pool) {
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  const app = express();
  app.use(express.json());
  app.use('/api/v1/auth', authRouter);
  return new Promise(r => {
    const srv = app.listen(0, '127.0.0.1', () => {
      r({ baseUrl: 'http://127.0.0.1:' + srv.address().port, close: () => new Promise(rs => srv.close(rs)) });
    });
  });
}

test('GET /auth/setup-required returns { required: true } on empty users (modulo dev seed)', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  const { baseUrl, close } = await appWithAuth(pool);
  try {
    const res = await fetch(baseUrl + '/api/v1/auth/setup-required');
    assert.equal(res.status, 200);
    assert.deepEqual(await res.json(), { required: true });
  } finally { await close(); await pool.end(); }
});

test('GET /auth/setup-required returns { required: false } once a real user exists', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  await pool.query(`INSERT INTO users (username, password_hash) VALUES ('admin', 'x')`);
  const { baseUrl, close } = await appWithAuth(pool);
  try {
    const res = await fetch(baseUrl + '/api/v1/auth/setup-required');
    assert.deepEqual(await res.json(), { required: false });
  } finally { await close(); await pool.end(); }
});

async function appWithSession(pool) {
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  process.env.SESSION_SECRET = 'test';
  process.env.AUTH_ENABLED = 'true';
  const ConnectPg = (await import('connect-pg-simple')).default(session);
  const app = express();
  app.use(express.json());
  app.use(session({
    store: new ConnectPg({ pool, tableName: 'sessions' }),
    secret: 'test', name: 'dragonflight.sid',
    cookie: { httpOnly: true, sameSite: 'lax', secure: false, maxAge: 8 * 3600 * 1000 },
    rolling: false, resave: false, saveUninitialized: false,
  }));
  app.use('/api/v1', requireUiHeader);
  app.use('/api/v1/auth', authRouter);
  return new Promise(r => {
    const srv = app.listen(0, '127.0.0.1', () => {
      r({ baseUrl: 'http://127.0.0.1:' + srv.address().port, close: () => new Promise(rs => srv.close(rs)) });
    });
  });
}

test('POST /auth/setup creates the first admin and returns a session cookie', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  const { baseUrl, close } = await appWithSession(pool);
  try {
    const res = await fetch(baseUrl + '/api/v1/auth/setup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'admin', password: 'correct-horse-battery' }),
    });
    assert.equal(res.status, 200);
    const body = await res.json();
    assert.equal(body.user.username, 'admin');
    assert.match(res.headers.get('set-cookie') || '', /dragonflight\.sid=/);
    const { rows } = await pool.query(`SELECT COUNT(*)::int AS n FROM users WHERE username='admin'`);
    assert.equal(rows[0].n, 1);
  } finally { await close(); await pool.end(); }
});

test('POST /auth/setup is 409 once a real user exists', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  await pool.query(`INSERT INTO users (username, password_hash) VALUES ('existing', 'x')`);
  const { baseUrl, close } = await appWithSession(pool);
  try {
    const res = await fetch(baseUrl + '/api/v1/auth/setup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'admin', password: 'correct-horse-battery' }),
    });
    assert.equal(res.status, 409);
    assert.equal((await res.json()).error, 'setup already complete');
  } finally { await close(); await pool.end(); }
});

test('POST /auth/setup rejects passwords shorter than 12 chars', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  const { baseUrl, close } = await appWithSession(pool);
  try {
    const res = await fetch(baseUrl + '/api/v1/auth/setup', {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'admin', password: 'short' }),
    });
    assert.equal(res.status, 400);
  } finally { await close(); await pool.end(); }
});

async function appWithSessionAndMe(pool) {
  // Same as appWithSession but also mounts a tiny /me endpoint behind requireAuth
  // so we can exercise the round-trip: login → cookie sent → /me 200.
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  process.env.AUTH_ENABLED = 'true';
  const ConnectPg = (await import('connect-pg-simple')).default(session);
  const app = express();
  app.use(express.json());
  app.use(session({
    store: new ConnectPg({ pool, tableName: 'sessions' }),
    secret: 'test', name: 'dragonflight.sid',
    cookie: { httpOnly: true, sameSite: 'lax', secure: false, maxAge: 8 * 3600 * 1000 },
    rolling: false, resave: false, saveUninitialized: false,
  }));
  app.use('/api/v1', requireUiHeader);
  app.use('/api/v1/auth', authRouter);
  app.get('/api/v1/protected/me', requireAuth, (req, res) => res.json({ user: req.user }));
  return new Promise(r => {
    const srv = app.listen(0, '127.0.0.1', () => {
      r({ baseUrl: 'http://127.0.0.1:' + srv.address().port, close: () => new Promise(rs => srv.close(rs)) });
    });
  });
}

test('POST /auth/login with valid creds → 200 + cookie, and the cookie unlocks subsequent requests (regression: redirect loop)', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  const hash = await hashPassword('correct-horse-battery');
  await pool.query(`INSERT INTO users (username, password_hash, display_name) VALUES ('alice', $1, 'Alice')`, [hash]);
  const { baseUrl, close } = await appWithSessionAndMe(pool);
  try {
    // 1. Login.
    const loginRes = await fetch(baseUrl + '/api/v1/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'alice', password: 'correct-horse-battery' }),
    });
    assert.equal(loginRes.status, 200);
    const setCookie = loginRes.headers.get('set-cookie');
    assert.match(setCookie || '', /dragonflight\.sid=/, 'expected Set-Cookie with dragonflight.sid');

    // 2. The SAME cookie must unlock the next request. This is the bug that
    //    produced the original redirect loop — login returned 200 but no cookie
    //    was persisted, so the next request 401'd and the SPA bounced.
    const meRes = await fetch(baseUrl + '/api/v1/protected/me', {
      headers: { cookie: setCookie.split(';')[0] },
    });
    assert.equal(meRes.status, 200, 'POST /login returned 200 but the cookie did not unlock /me — this is the regression');
    assert.equal((await meRes.json()).user.username, 'alice');
  } finally { await close(); await pool.end(); }
});

test('POST /auth/login with wrong password → 401 + generic message (no enumeration)', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  const hash = await hashPassword('correct-horse-battery');
  await pool.query(`INSERT INTO users (username, password_hash) VALUES ('alice', $1)`, [hash]);
  const { baseUrl, close } = await appWithSessionAndMe(pool);
  try {
    const r1 = await fetch(baseUrl + '/api/v1/auth/login', {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'alice', password: 'wrong' }),
    });
    const r2 = await fetch(baseUrl + '/api/v1/auth/login', {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'nobody', password: 'whatever-long-enough' }),
    });
    assert.equal(r1.status, 401);
    assert.equal(r2.status, 401);
    const e1 = (await r1.json()).error, e2 = (await r2.json()).error;
    assert.equal(e1, 'invalid credentials');
    assert.equal(e2, 'invalid credentials');  // identical message — no enumeration
  } finally { await close(); await pool.end(); }
});

test('POST /auth/logout destroys the session row and the cookie no longer unlocks /me', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  const hash = await hashPassword('correct-horse-battery');
  await pool.query(`INSERT INTO users (username, password_hash) VALUES ('alice', $1)`, [hash]);
  const { baseUrl, close } = await appWithSessionAndMe(pool);
  try {
    const loginRes = await fetch(baseUrl + '/api/v1/auth/login', {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'alice', password: 'correct-horse-battery' }),
    });
    const cookie = (loginRes.headers.get('set-cookie') || '').split(';')[0];

    const logoutRes = await fetch(baseUrl + '/api/v1/auth/logout', {
      method: 'POST', headers: { cookie },
    });
    assert.equal(logoutRes.status, 204);

    const meRes = await fetch(baseUrl + '/api/v1/protected/me', { headers: { cookie } });
    assert.equal(meRes.status, 401);
  } finally { await close(); await pool.end(); }
});

test('GET /auth/me returns the authed user', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  const hash = await hashPassword('correct-horse-battery');
  await pool.query(`INSERT INTO users (username, password_hash, display_name) VALUES ('alice', $1, 'Alice')`, [hash]);
  const { baseUrl, close } = await appWithSessionAndMe(pool);
  try {
    const loginRes = await fetch(baseUrl + '/api/v1/auth/login', {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'alice', password: 'correct-horse-battery' }),
    });
    const cookie = (loginRes.headers.get('set-cookie') || '').split(';')[0];
    const me = await fetch(baseUrl + '/api/v1/auth/me', { headers: { cookie } });
    assert.equal(me.status, 200);
    const body = await me.json();
    assert.equal(body.username, 'alice');
    assert.equal(body.display_name, 'Alice');
  } finally { await close(); await pool.end(); }
});

test('POST /auth/password rotates the password when current is correct', { skip: !isTestDbConfigured() && 'TEST_DATABASE_URL not set' }, async () => {
  const pool = await setupTestDb();
  const hash = await hashPassword('correct-horse-battery');
  await pool.query(`INSERT INTO users (username, password_hash) VALUES ('alice', $1)`, [hash]);
  const { baseUrl, close } = await appWithSessionAndMe(pool);
  try {
    const loginRes = await fetch(baseUrl + '/api/v1/auth/login', {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
      body: JSON.stringify({ username: 'alice', password: 'correct-horse-battery' }),
    });
    const cookie = (loginRes.headers.get('set-cookie') || '').split(';')[0];
    const change = await fetch(baseUrl + '/api/v1/auth/password', {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui', cookie },
      body: JSON.stringify({ current_password: 'correct-horse-battery', new_password: 'brand-new-passphrase' }),
    });
    assert.equal(change.status, 204);
    // Wrong current → 400
    const wrong = await fetch(baseUrl + '/api/v1/auth/password', {
      method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui', cookie },
      body: JSON.stringify({ current_password: 'no', new_password: 'another-good-passphrase' }),
    });
    assert.equal(wrong.status, 400);
  } finally { await close(); await pool.end(); }
});
