const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const { JSDOM } = require('jsdom');
const test = require('node:test');
const assert = require('node:assert/strict');

const handoffSource = fs.readFileSync(path.join(__dirname, '..', 'js', 'credential-vault-handoff.js'), 'utf8');
const formSource = fs.readFileSync(path.join(__dirname, '..', 'js', 'service-credentials.js'), 'utf8');
const initSource = fs.readFileSync(path.join(__dirname, '..', 'js', 'core', 'init.js'), 'utf8');

function loadVault() {
  const window = { location: { origin: 'https://status.sami' } };
  const context = vm.createContext({ window, SITE: { tld: '.sami' }, URL });
  vm.runInContext(handoffSource, context);
  return window.DCCredentialVault;
}

async function exerciseFailedModalWrite({
  service,
  fetchJson,
  setupInputs,
  expectedEndpoint,
  writeResponse,
  expectedError = /vault write rejected/,
}) {
  const dom = new JSDOM('<!doctype html><body></body>', {
    url: 'https://status.sami/',
    runScripts: 'outside-only',
  });
  const { window } = dom;
  const writeUrls = [];
  let resumeCalls = 0;
  window.ErrorHandler = class { logError() {} };
  window.SITE = { tld: '.sami' };
  window.injectModal = (_id, html) => window.document.body.insertAdjacentHTML('beforeend', html);
  window.fetch = async (url) => ({ ok: true, json: async () => fetchJson(url) });
  window.secureFetch = async (url) => {
    writeUrls.push(url);
    return writeResponse || {
      ok: false,
      status: 500,
      json: async () => ({ success: false, error: 'vault write rejected' }),
    };
  };
  window.DCCredentialVault = {
    isAllowedReturnUrl: () => true,
    resume: async () => { resumeCalls++; return true; },
  };
  window.confirm = () => true;
  window.eval(formSource);

  await window.openServiceCredsModal(service, { returnUrl: `https://${service.id}.sami/` });
  setupInputs(window.document);
  window.document.getElementById('svc-creds-save').click();
  await new Promise(resolve => setTimeout(resolve, 20));

  assert.equal(writeUrls[0], expectedEndpoint);
  assert.equal(resumeCalls, 0);
  assert.match(window.document.getElementById('svc-creds-error').textContent, expectedError);
}

test('existing dashboard session mints a one-time token and resumes on the target host', async () => {
  const vault = loadVault();
  const calls = [];
  const replacements = [];
  const resumed = await vault.resume('https://plex.sami/web/?direct=1#home', 'plex', {
    fetch: async (url, options) => {
      calls.push({ url, options });
      return {
        ok: true,
        json: async () => ({ success: true, ssoToken: 'one-time-token' }),
      };
    },
    location: { replace: (target) => replacements.push(target) },
  });

  assert.equal(resumed, true);
  assert.equal(calls.length, 1);
  assert.equal(calls[0].url, '/api/v1/auth/sso-handoff?serviceId=plex');
  assert.equal(calls[0].options.credentials, 'include');
  assert.equal(calls[0].options.cache, 'no-store');
  assert.equal(
    replacements[0],
    'https://plex.sami/dashcaddy-sso?token=one-time-token&return=%2Fweb%2F%3Fdirect%3D1%23home',
  );
});

test('vault handoff rejects an external return URL before minting a token', async () => {
  const vault = loadVault();
  let fetchCalled = false;
  const resumed = await vault.resume('https://plex.sami.evil.example/phish', 'plex', {
    fetch: async () => { fetchCalled = true; },
    location: { replace: () => assert.fail('must not navigate') },
  });

  assert.equal(resumed, false);
  assert.equal(fetchCalled, false);
});

test('credential request opens the form and save path calls the tested handoff helper', () => {
  assert.match(formSource, /params\.get\('credentials'\)/);
  assert.match(formSource, /openServiceCredsModal\(service, \{ returnUrl \}\)/);
  assert.match(formSource, /DCCredentialVault\?\.resume\(returnUrl, currentService\.id\)/);
  assert.match(initSource, /openRequestedCredentialForm\(\)/);
  assert.match(formSource, /Save to vault & open service/);
});

test('actual vault modal save handler stores credentials then resumes the handoff', async () => {
  const dom = new JSDOM('<!doctype html><body></body>', {
    url: 'https://status.sami/?credentials=plex&return=https%3A%2F%2Fplex.sami%2Fweb%2F',
    runScripts: 'outside-only',
  });
  const { window } = dom;
  let stored = false;
  const writes = [];
  const resumed = [];
  window.ErrorHandler = class { logError() {} };
  window.SITE = { tld: '.sami' };
  window.APPS = [{ id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' }];
  window.injectModal = (_id, html) => window.document.body.insertAdjacentHTML('beforeend', html);
  window.fetch = async () => ({
    ok: true,
    json: async () => ({
      success: true,
      hasApiKey: false,
      hasBasicAuth: stored,
      username: stored ? 'vault-user' : null,
    }),
  });
  window.secureFetch = async (url, options) => {
    writes.push({ url, body: JSON.parse(options.body) });
    stored = true;
    return { ok: true, json: async () => ({ success: true }) };
  };
  window.DCCredentialVault = {
    isAllowedReturnUrl: () => true,
    resume: async (returnUrl, serviceId) => { resumed.push({ returnUrl, serviceId }); return true; },
  };
  window.confirm = () => true;
  window.eval(formSource);

  await window.openServiceCredsModal(window.APPS[0], { returnUrl: 'https://plex.sami/web/' });
  window.document.getElementById('svc-basic-user').value = 'vault-user';
  window.document.getElementById('svc-basic-pass').value = 'vault-password';
  window.document.getElementById('svc-creds-save').click();
  await new Promise(resolve => setTimeout(resolve, 20));

  assert.deepEqual(writes, [{
    url: '/api/v1/services/plex/credentials',
    body: { username: 'vault-user', password: 'vault-password' },
  }]);
  assert.deepEqual(resumed, [{ returnUrl: 'https://plex.sami/web/', serviceId: 'plex' }]);
});

test('failed credential write does not mint a handoff or navigate', async () => {
  const dom = new JSDOM('<!doctype html><body></body>', {
    url: 'https://status.sami/',
    runScripts: 'outside-only',
  });
  const { window } = dom;
  let resumeCalls = 0;
  window.ErrorHandler = class { logError() {} };
  window.SITE = { tld: '.sami' };
  window.injectModal = (_id, html) => window.document.body.insertAdjacentHTML('beforeend', html);
  window.fetch = async () => ({
    ok: true,
    json: async () => ({ success: true, hasApiKey: false, hasBasicAuth: false, username: null }),
  });
  window.secureFetch = async () => ({
    ok: false,
    status: 500,
    json: async () => ({ success: false, error: 'vault write rejected' }),
  });
  window.DCCredentialVault = {
    isAllowedReturnUrl: () => true,
    resume: async () => { resumeCalls++; return true; },
  };
  window.confirm = () => true;
  window.eval(formSource);

  const service = { id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' };
  await window.openServiceCredsModal(service, { returnUrl: 'https://plex.sami/web/' });
  window.document.getElementById('svc-basic-user').value = 'vault-user';
  window.document.getElementById('svc-basic-pass').value = 'vault-password';
  window.document.getElementById('svc-creds-save').click();
  await new Promise(resolve => setTimeout(resolve, 20));

  assert.equal(resumeCalls, 0);
  assert.match(window.document.getElementById('svc-creds-error').textContent, /vault write rejected/);
});

test('failed ARR credential write does not mint a handoff or navigate', async () => {
  await exerciseFailedModalWrite({
    service: { id: 'radarr', name: 'Radarr', appTemplate: 'radarr', url: 'https://radarr.sami' },
    fetchJson: (url) => url.includes('/services/')
      ? { success: true, hasApiKey: false, hasBasicAuth: false, username: null }
      : { success: true, profiles: [] },
    setupInputs: (document) => { document.getElementById('svc-apikey-input').value = 'arr-key'; },
    expectedEndpoint: '/api/v1/arr/credentials',
  });
});

test('failed ARR quality-profile write does not mint a handoff or navigate', async () => {
  await exerciseFailedModalWrite({
    service: { id: 'radarr', name: 'Radarr', appTemplate: 'radarr', url: 'https://radarr.sami' },
    fetchJson: (url) => url.includes('/services/')
      ? { success: true, hasApiKey: true, hasBasicAuth: false, username: null }
      : { success: true, profiles: [{ id: 1, name: 'Default' }], storedProfileId: 1 },
    setupInputs: () => {},
    expectedEndpoint: '/api/v1/arr/quality-profiles',
  });
});

test('failed seedhost write does not mint a handoff or navigate', async () => {
  await exerciseFailedModalWrite({
    service: { id: 'torrent', name: 'qBittorrent', isExternal: true, externalUrl: 'https://torrent.sami' },
    fetchJson: (url) => url.includes('/seedhost-creds')
      ? { success: true, hasCredentials: false, username: null }
      : { success: true, hasApiKey: false, hasBasicAuth: false, username: null },
    setupInputs: (document) => {
      document.getElementById('svc-seedhost-user').value = 'seed-user';
      document.getElementById('svc-seedhost-pass').value = 'seed-password';
    },
    expectedEndpoint: '/api/v1/seedhost-creds',
  });
});

test('failed generic API-key write does not mint a handoff or navigate', async () => {
  await exerciseFailedModalWrite({
    service: { id: 'custom', name: 'Custom', url: 'https://custom.sami' },
    fetchJson: () => ({ success: true, hasApiKey: false, hasBasicAuth: false, username: null }),
    setupInputs: (document) => {
      document.getElementById('svc-apikey-input').value = 'custom-key';
      document.getElementById('svc-basic-user').value = 'user';
      document.getElementById('svc-basic-pass').value = 'password';
    },
    expectedEndpoint: '/api/v1/services/custom/credentials',
  });
});

test('HTTP 2xx with malformed JSON does not mint a handoff or navigate', async () => {
  await exerciseFailedModalWrite({
    service: { id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' },
    fetchJson: () => ({ success: true, hasApiKey: false, hasBasicAuth: false, username: null }),
    setupInputs: (document) => {
      document.getElementById('svc-basic-user').value = 'user';
      document.getElementById('svc-basic-pass').value = 'password';
    },
    expectedEndpoint: '/api/v1/services/plex/credentials',
    writeResponse: { ok: true, status: 200, json: async () => { throw new Error('bad json'); } },
    expectedError: /invalid server response/,
  });
});

test('HTTP 2xx without success:true does not mint a handoff or navigate', async () => {
  await exerciseFailedModalWrite({
    service: { id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' },
    fetchJson: () => ({ success: true, hasApiKey: false, hasBasicAuth: false, username: null }),
    setupInputs: (document) => {
      document.getElementById('svc-basic-user').value = 'user';
      document.getElementById('svc-basic-pass').value = 'password';
    },
    expectedEndpoint: '/api/v1/services/plex/credentials',
    writeResponse: { ok: true, status: 200, json: async () => ({ message: 'ambiguous' }) },
    expectedError: /failed \(200\)/,
  });
});

test('failed credential clear remains visibly failed and keeps stored-state UI', async () => {
  const dom = new JSDOM('<!doctype html><body><button id="creds-btn-plex" class="has-creds"></button></body>', {
    url: 'https://status.sami/',
    runScripts: 'outside-only',
  });
  const { window } = dom;
  window.ErrorHandler = class { logError() {} };
  window.SITE = { tld: '.sami' };
  window.injectModal = (_id, html) => window.document.body.insertAdjacentHTML('beforeend', html);
  window.fetch = async () => ({
    ok: true,
    json: async () => ({ success: true, hasApiKey: false, hasBasicAuth: true, username: 'vault-user' }),
  });
  window.secureFetch = async () => ({
    ok: false,
    status: 500,
    json: async () => ({ success: false, error: 'clear rejected' }),
  });
  window.DCCredentialVault = { isAllowedReturnUrl: () => false };
  window.confirm = () => true;
  window.eval(formSource);

  const service = { id: 'plex', name: 'Plex', appTemplate: 'plex', url: 'https://plex.sami' };
  await window.openServiceCredsModal(service);
  window.document.getElementById('svc-creds-clear').click();
  await new Promise(resolve => setTimeout(resolve, 20));

  assert.match(window.document.getElementById('svc-creds-error').textContent, /clear rejected/);
  assert.equal(window.document.getElementById('creds-btn-plex').classList.contains('has-creds'), true);
});

test('handoff rejects a private-TLD host that is not the requested protected service', async () => {
  const vault = loadVault();
  let fetchCalled = false;
  const resumed = await vault.resume('https://dns1.sami/', 'plex', {
    fetch: async () => { fetchCalled = true; },
    location: { replace: () => assert.fail('must not navigate') },
  });

  assert.equal(resumed, false);
  assert.equal(fetchCalled, false);
});
