/**
 * Tests for the Teltonika API client token lifecycle.
 * All HTTP calls are intercepted with a mock axios instance.
 */

const { test, before, beforeEach } = require('node:test');
const assert = require('node:assert/strict');

// Provide an in-memory DB so db.js doesn't complain
process.env.DB_PATH        = ':memory:';
process.env.ADMIN_USERNAME = 'admin';
process.env.ADMIN_PASSWORD = 'test';

const db = require('../src/db');
db.initDB();
db.setConfig('device_ip',       '192.168.1.1');
db.setConfig('device_username', 'admin');
db.setConfig('device_password', 'secret');
db.setConfig('device_modem',    '1-1');

// ── Minimal axios mock ────────────────────────────────────────────────────────
// We monkey-patch require cache so teltonika.js picks up our mock.

let mockResponses = [];   // queue of { status, data } or Error
let requestLog    = [];   // recorded calls

function mockAxios(config) {
  requestLog.push({ method: config.method?.toUpperCase(), url: config.url, data: config.data, params: config.params });
  const next = mockResponses.shift();
  if (!next) throw new Error('No mock response queued');
  if (next instanceof Error) return Promise.reject(next);
  if (next.status === 401) {
    const err = new Error('Request failed with status code 401');
    err.response = { status: 401 };
    return Promise.reject(err);
  }
  return Promise.resolve({ status: next.status, data: next.data });
}
mockAxios.post = (url, data, opts) => mockAxios({ method: 'POST', url, data });

// Inject mock before requiring teltonika
require.cache[require.resolve('axios')] = { exports: mockAxios };

// Now load the client (fresh each test via cache manipulation)
function freshClient() {
  // Remove cached module so we get a new singleton
  delete require.cache[require.resolve('../src/teltonika')];
  return require('../src/teltonika');
}

beforeEach(() => {
  mockResponses = [];
  requestLog    = [];
});

// ── Helpers ───────────────────────────────────────────────────────────────────

function loginResponse(expiresIn = 300) {
  return { status: 200, data: { data: { token: 'tok-abc123', expires: expiresIn } } };
}

// ── Tests ─────────────────────────────────────────────────────────────────────

test('login: obtains token and sets expiry', async () => {
  const client = freshClient();
  mockResponses.push(loginResponse(300));

  await client.login();

  assert.equal(client.token, 'tok-abc123');
  assert.ok(client.tokenExpires > Date.now());
  assert.ok(client.tokenExpires <= Date.now() + 300_000 + 100);
});

test('_ensureToken: skips login when token is still valid', async () => {
  const client = freshClient();
  // Pre-set a valid token (expires far in the future)
  client.token        = 'existing-token';
  client.tokenExpires = Date.now() + 600_000;

  await client._ensureToken();

  // No login call should have been made
  assert.equal(requestLog.length, 0);
  assert.equal(client.token, 'existing-token');
});

test('_ensureToken: triggers login when token is null', async () => {
  const client = freshClient();
  mockResponses.push(loginResponse());

  await client._ensureToken();

  assert.equal(requestLog.length, 1);
  assert.ok(requestLog[0].url.endsWith('/login'));
  assert.equal(client.token, 'tok-abc123');
});

test('_ensureToken: triggers login when token is expired', async () => {
  const client = freshClient();
  client.token        = 'old-token';
  client.tokenExpires = Date.now() - 1_000;  // already expired

  mockResponses.push(loginResponse());
  await client._ensureToken();

  assert.equal(client.token, 'tok-abc123');
  assert.equal(requestLog.length, 1);
});

test('_ensureToken: triggers login when within 30s proactive window', async () => {
  const client = freshClient();
  client.token        = 'expiring-soon';
  client.tokenExpires = Date.now() + 20_000;  // 20 s left → inside 30 s window

  mockResponses.push(loginResponse());
  await client._ensureToken();

  assert.equal(client.token, 'tok-abc123');
});

test('concurrent _ensureToken calls share a single login request', async () => {
  const client = freshClient();
  // Only one login response queued — if two logins fired it would throw
  mockResponses.push(loginResponse());

  // Fire three concurrent _ensureToken calls
  await Promise.all([
    client._ensureToken(),
    client._ensureToken(),
    client._ensureToken(),
  ]);

  // Exactly one POST /login
  const loginCalls = requestLog.filter(r => r.url?.endsWith('/login'));
  assert.equal(loginCalls.length, 1);
});

test('_request: re-authenticates and retries on 401 response', async () => {
  const client = freshClient();
  // Give it a seemingly-valid token
  client.token        = 'stale-token';
  client.tokenExpires = Date.now() + 600_000;

  // First API call → 401
  mockResponses.push({ status: 401 });
  // Re-login
  mockResponses.push(loginResponse());
  // Retry API call → success
  mockResponses.push({ status: 200, data: { success: true } });

  const response = await client._request('GET', '/messages');

  assert.equal(response.data.success, true);

  // Verify re-login happened
  const loginCalls = requestLog.filter(r => r.url?.endsWith('/login'));
  assert.equal(loginCalls.length, 1);

  // Verify the original path was called twice (first attempt + retry)
  const msgCalls = requestLog.filter(r => r.url?.endsWith('/messages'));
  assert.equal(msgCalls.length, 2);
});

test('_request: does not retry on non-401 errors', async () => {
  const client = freshClient();
  client.token        = 'valid-token';
  client.tokenExpires = Date.now() + 600_000;

  const networkErr = new Error('ECONNREFUSED');
  networkErr.code   = 'ECONNREFUSED';
  mockResponses.push(networkErr);

  await assert.rejects(
    () => client._request('GET', '/messages'),
    /ECONNREFUSED/
  );

  // No re-login
  const loginCalls = requestLog.filter(r => r.url?.endsWith('/login'));
  assert.equal(loginCalls.length, 0);
});

test('testConnection: clears existing token before testing', async () => {
  const client = freshClient();
  client.token        = 'old';
  client.tokenExpires = Date.now() + 999_999;

  mockResponses.push(loginResponse());
  const result = await client.testConnection();

  assert.equal(result.success, true);
  assert.equal(client.token, 'tok-abc123');
});

test('testConnection: returns success:false on failure', async () => {
  const client = freshClient();
  mockResponses.push(new Error('Network unreachable'));

  const result = await client.testConnection();

  assert.equal(result.success, false);
  assert.ok(result.error);
});

test('readMessages: sends modem query parameter', async () => {
  const client = freshClient();
  client.token        = 'valid-token';
  client.tokenExpires = Date.now() + 600_000;

  mockResponses.push({ status: 200, data: { data: [] } });

  await client.readMessages();

  const call = requestLog.find(r => r.url?.endsWith('/messages'));
  assert.ok(call, 'GET /messages was not called');
  assert.deepEqual(call.params, { modem: '1-1' });
});

test('readMessages: returns empty array when device returns no messages', async () => {
  const client = freshClient();
  client.token        = 'valid-token';
  client.tokenExpires = Date.now() + 600_000;

  mockResponses.push({ status: 200, data: { data: [] } });
  const messages = await client.readMessages();

  assert.deepEqual(messages, []);
});

test('readMessages: parses messages from data array', async () => {
  const client = freshClient();
  client.token        = 'valid-token';
  client.tokenExpires = Date.now() + 600_000;

  mockResponses.push({ status: 200, data: { data: [
    { id: 1, sender: '+46701234567', text: 'Hello', date: '2026-01-01 10:00:00', read: 1, type: 'inbox' },
    { id: 2, sender: '+46709999999', text: 'World', date: '2026-01-01 11:00:00', read: 0, type: 'inbox' },
  ]}});

  const messages = await client.readMessages();

  assert.equal(messages.length, 2);
  assert.equal(messages[0].id,     '1');
  assert.equal(messages[0].sender, '+46701234567');
  assert.equal(messages[0].text,   'Hello');
  assert.equal(messages[0].type,   'inbox');
});
