import { describe, expect, test } from 'bun:test';
import { guardDeletable, guardMemberTree, parseSubUidEntry } from './os-user-deprovision';

// The guards that stand between a database column and `sudo userdel` / `sudo chown -R`.
//
// Everything else in os-user-deprovision.ts needs a machine with accounts to delete on. These three are pure
// precisely so the dangerous decisions can be tested without one — they are the part where being wrong is not
// recoverable, and the manual teardown that produced the spec is not a thing anyone should repeat to check a
// refactor.

describe('guardDeletable', () => {
  const home = '/data/member@example.com/home';

  test('accepts an account the platform made', () => {
    expect(guardDeletable({ osUser: 'green', uid: 1002, passwdHome: home, expectedHome: home })).toEqual({ ok: true });
  });

  test('refuses root, and every other system account', () => {
    // The scenario: a `users` row whose osUser column says 'root'. Nothing else in the sequence would stop it
    // — `loginctl`, `pkill` and `userdel` would all simply do as they were told.
    const result = guardDeletable({ osUser: 'root', uid: 0, passwdHome: '/root', expectedHome: home });
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.error).toContain('system account');

    expect(guardDeletable({ osUser: 'daemon', uid: 1, passwdHome: home, expectedHome: home }).ok).toBe(false);
    expect(guardDeletable({ osUser: 'nobody', uid: 999, passwdHome: home, expectedHome: home }).ok).toBe(false);
  });

  test('refuses a same-named account that is not ours', () => {
    // A human account that happens to share a member's username. Its home is its own, so it fails the only
    // test that proves ownership. This is `ensureOsUser`'s adoption rule read backwards.
    const result = guardDeletable({ osUser: 'green', uid: 1002, passwdHome: '/home/green', expectedHome: home });
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.error).toContain('not an account the platform created');
  });

  test('refuses an account with no passwd home at all', () => {
    expect(guardDeletable({ osUser: 'green', uid: 1002, passwdHome: null, expectedHome: home }).ok).toBe(false);
  });

  test('the uid floor is not sufficient on its own', () => {
    // Both halves are required: a high uid with a foreign home is still somebody else's account.
    expect(guardDeletable({ osUser: 'green', uid: 4000, passwdHome: '/srv/green', expectedHome: home }).ok).toBe(false);
  });
});

describe('guardMemberTree', () => {
  test('resolves a normal account directory', () => {
    const result = guardMemberTree('/data', 'member@example.com');
    expect(result).toEqual({ ok: true, tree: '/data/member@example.com' });
  });

  test('refuses traversal out of DATA_PATH', () => {
    // The path this produces is the argument to `chown -R` and, under destroy, to a recursive delete. The
    // email reaches join() from a database row.
    expect(guardMemberTree('/data', '../../etc').ok).toBe(false);
    expect(guardMemberTree('/data', '..').ok).toBe(false);
    expect(guardMemberTree('/data', 'a/../../b').ok).toBe(false);
  });

  test('refuses a nested path even without traversal', () => {
    // A member tree is one level down. Anything deeper is not an account directory, whatever it is.
    expect(guardMemberTree('/data', 'a/b').ok).toBe(false);
  });

  test('refuses DATA_PATH itself', () => {
    expect(guardMemberTree('/data', '').ok).toBe(false);
    expect(guardMemberTree('/data', '.').ok).toBe(false);
  });

  test('refuses an empty DATA_PATH', () => {
    // Unset DATA_PATH would otherwise make every member tree a path under the process's cwd.
    expect(guardMemberTree('', 'member@example.com').ok).toBe(false);
  });
});

describe('parseSubUidEntry', () => {
  const file = 'pastilhas:100000:65536\ngreen:231072:65536\nofficer_jg:165536:65536\n';

  test('reads the range for the named account only', () => {
    expect(parseSubUidEntry(file, 'green')).toEqual({ start: 231072, count: 65536 });
    expect(parseSubUidEntry(file, 'officer_jg')).toEqual({ start: 165536, count: 65536 });
  });

  test('a missing entry is null, not a guess', () => {
    // Normal: rootless Docker is tolerated when it fails, and accounts predating it have no entry. A guessed
    // range would make the audit check a different range than the one that was actually freed.
    expect(parseSubUidEntry(file, 'nobody')).toBeNull();
    expect(parseSubUidEntry('', 'green')).toBeNull();
  });

  test('does not match on a prefix', () => {
    // 'green' must not match the 'green2' line — the ranges are different and the wrong one verifies nothing.
    expect(parseSubUidEntry('green2:300000:65536\n', 'green')).toBeNull();
  });

  test('rejects a malformed line rather than producing NaN', () => {
    // NaN would flow into `assert-uid-free.sh --check` as an argument and quietly scan nothing.
    expect(parseSubUidEntry('green:notanumber:65536\n', 'green')).toBeNull();
    expect(parseSubUidEntry('green:231072:0\n', 'green')).toBeNull();
  });
});
