import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
  createDefaultSyncHostsState,
  getSyncHostsPath,
  isValidSyncHost,
  normalizeSyncHostsState,
  readSyncHostsState,
  recordSyncResult,
  removeSyncHost,
  upsertSyncHost,
  writeSyncHostsState,
} from './sync-hosts-store';

function withTempDir(fn: (dir: string) => void): void {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-sync-hosts-test-'));
  try {
    fn(dir);
  } finally {
    fs.rmSync(dir, { recursive: true, force: true });
  }
}

test('createDefaultSyncHostsState returns empty v1 state', () => {
  assert.deepEqual(createDefaultSyncHostsState(), {
    version: 1,
    autoSyncIntervalMinutes: 60,
    hosts: [],
  });
});

test('isValidSyncHost accepts ssh destinations and rejects unsafe values', () => {
  assert.equal(isValidSyncHost('desktop'), true);
  assert.equal(isValidSyncHost('user@10.0.0.5'), true);
  assert.equal(isValidSyncHost('my-alias.local'), true);
  assert.equal(isValidSyncHost(''), false);
  assert.equal(isValidSyncHost('   '), false);
  assert.equal(isValidSyncHost('-oProxyCommand=evil'), false);
  assert.equal(isValidSyncHost('host with spaces'), false);
});

test('upsertSyncHost adds a new host with defaults and preserves fields on update', () => {
  const state = createDefaultSyncHostsState();
  const added = upsertSyncHost(state, { host: 'user@laptop' }, 1000);
  assert.deepEqual(added.hosts, [
    {
      host: 'user@laptop',
      label: null,
      direction: 'both',
      autoSync: false,
      addedAtMs: 1000,
      lastSyncAtMs: null,
      lastSyncStatus: null,
      lastSyncDetail: null,
    },
  ]);
  // original state untouched
  assert.equal(state.hosts.length, 0);

  const synced = recordSyncResult(added, 'user@laptop', {
    atMs: 2000,
    status: 'success',
    detail: 'Sessions merged: 3',
  });
  const updated = upsertSyncHost(synced, { host: 'user@laptop', direction: 'push' }, 3000);
  assert.equal(updated.hosts.length, 1);
  const entry = updated.hosts[0]!;
  assert.equal(entry.direction, 'push');
  assert.equal(entry.addedAtMs, 1000);
  assert.equal(entry.lastSyncAtMs, 2000);
  assert.equal(entry.lastSyncStatus, 'success');
  assert.equal(entry.lastSyncDetail, 'Sessions merged: 3');
});

test('upsertSyncHost trims host and throws on invalid host', () => {
  const state = upsertSyncHost(createDefaultSyncHostsState(), { host: '  desktop  ' }, 1);
  assert.equal(state.hosts[0]!.host, 'desktop');
  assert.throws(() => upsertSyncHost(state, { host: '-bad' }, 2));
});

test('removeSyncHost removes only the matching host', () => {
  let state = createDefaultSyncHostsState();
  state = upsertSyncHost(state, { host: 'a' }, 1);
  state = upsertSyncHost(state, { host: 'b' }, 2);
  const removed = removeSyncHost(state, 'a');
  assert.deepEqual(
    removed.hosts.map((entry) => entry.host),
    ['b'],
  );
  assert.equal(removeSyncHost(removed, 'missing').hosts.length, 1);
});

test('recordSyncResult upserts unknown hosts so CLI syncs are remembered', () => {
  const state = recordSyncResult(createDefaultSyncHostsState(), 'user@server', {
    atMs: 5000,
    status: 'error',
    detail: 'Remote merge failed',
  });
  assert.equal(state.hosts.length, 1);
  const entry = state.hosts[0]!;
  assert.equal(entry.host, 'user@server');
  assert.equal(entry.addedAtMs, 5000);
  assert.equal(entry.lastSyncAtMs, 5000);
  assert.equal(entry.lastSyncStatus, 'error');
  assert.equal(entry.lastSyncDetail, 'Remote merge failed');
});

test('normalizeSyncHostsState drops invalid entries and falls back to defaults', () => {
  assert.deepEqual(normalizeSyncHostsState(null), createDefaultSyncHostsState());
  assert.deepEqual(normalizeSyncHostsState('junk'), createDefaultSyncHostsState());
  assert.deepEqual(normalizeSyncHostsState({ version: 99 }), createDefaultSyncHostsState());

  const normalized = normalizeSyncHostsState({
    version: 1,
    autoSyncIntervalMinutes: 15,
    hosts: [
      {
        host: 'desktop',
        label: 'Desktop PC',
        direction: 'pull',
        autoSync: true,
        addedAtMs: 10,
        lastSyncAtMs: 20,
        lastSyncStatus: 'success',
        lastSyncDetail: 'ok',
      },
      { host: '-bad', direction: 'both', autoSync: false, addedAtMs: 1 },
      { host: 'dupe', direction: 'sideways', autoSync: false, addedAtMs: 1 },
      'not-an-object',
    ],
  });
  assert.equal(normalized.autoSyncIntervalMinutes, 15);
  assert.deepEqual(normalized.hosts, [
    {
      host: 'desktop',
      label: 'Desktop PC',
      direction: 'pull',
      autoSync: true,
      addedAtMs: 10,
      lastSyncAtMs: 20,
      lastSyncStatus: 'success',
      lastSyncDetail: 'ok',
    },
    {
      host: 'dupe',
      label: null,
      direction: 'both',
      autoSync: false,
      addedAtMs: 1,
      lastSyncAtMs: null,
      lastSyncStatus: null,
      lastSyncDetail: null,
    },
  ]);
});

test('read/write round-trips state and tolerates missing or corrupt files', () => {
  withTempDir((root) => {
    const filePath = getSyncHostsPath(root);
    assert.equal(filePath, path.join(root, 'sync-hosts.json'));
    assert.deepEqual(readSyncHostsState(filePath), createDefaultSyncHostsState());

    fs.writeFileSync(filePath, '{corrupt');
    assert.deepEqual(readSyncHostsState(filePath), createDefaultSyncHostsState());

    let state = createDefaultSyncHostsState();
    state = upsertSyncHost(state, { host: 'user@laptop', label: 'Laptop' }, 42);
    writeSyncHostsState(filePath, state);
    assert.deepEqual(readSyncHostsState(filePath), state);
  });
});

test('writeSyncHostsState creates parent directories', () => {
  withTempDir((root) => {
    const filePath = path.join(root, 'nested', 'dir', 'sync-hosts.json');
    writeSyncHostsState(filePath, createDefaultSyncHostsState());
    assert.deepEqual(readSyncHostsState(filePath), createDefaultSyncHostsState());
  });
});

test('writeSyncHostsState leaves the previous file intact when the write fails', () => {
  withTempDir((root) => {
    const filePath = getSyncHostsPath(root);
    let state = createDefaultSyncHostsState();
    state = upsertSyncHost(state, { host: 'user@laptop', label: 'Laptop' }, 42);
    writeSyncHostsState(filePath, state);

    // A crash mid-write must not truncate the live file: readSyncHostsState
    // falls back to defaults on a corrupt file, which would drop every host.
    let next = upsertSyncHost(state, { host: 'user@desktop' }, 43);
    assert.throws(() =>
      writeSyncHostsState(filePath, next, {
        renameSync: () => {
          throw new Error('disk full');
        },
      }),
    );
    assert.deepEqual(readSyncHostsState(filePath), state);
    assert.equal(fs.existsSync(`${filePath}.tmp`), false);
  });
});
