import {
  ADMIN_COOKIE_OPTIONS,
  newToken,
  passwordSetByOther,
  recordCodeFailure,
  requesterAuditContext,
  setAdminCookie,
  setPasswordChangeRequired,
  tokenHash,
} from './admin-session';
import { MySqlDialect } from 'drizzle-orm/mysql-core';

describe('관리자 세션 쿠키 · 토큰 (계획 3.8)', () => {
  it('쿠키 옵션은 네 신원 같은 값이고 Max-Age · Expires 가 없다(세션 쿠키)', () => {
    expect(ADMIN_COOKIE_OPTIONS).toEqual({
      httpOnly: true,
      secure: true,
      sameSite: 'none',
      path: '/',
    });
    expect(ADMIN_COOKIE_OPTIONS).not.toHaveProperty('maxAge');
    expect(ADMIN_COOKIE_OPTIONS).not.toHaveProperty('expires');
  });

  it('setAdminCookie 는 _aaT 에 그 옵션 그대로 싣는다', () => {
    const res = { cookie: jest.fn() };
    setAdminCookie(res as any, 'tok');
    expect(res.cookie).toHaveBeenCalledWith('_aaT', 'tok', ADMIN_COOKIE_OPTIONS);
  });

  it('newToken 은 32바이트 base64url 이고 매번 다르다 · tokenHash 는 원문이 아닌 sha256 hex', () => {
    const a = newToken();
    expect(a).toMatch(/^[A-Za-z0-9_-]{43}$/);
    expect(newToken()).not.toBe(a);
    expect(tokenHash(a)).toMatch(/^[0-9a-f]{64}$/);
    expect(tokenHash(a)).not.toContain(a);
  });
});

describe('남이 정한 비밀번호 표지의 감사 (계획 3.19 설정 경로)', () => {
  it('표지를 쓰고 같은 tx 로 password.change-required 행을 요청자 신원으로 남긴다', async () => {
    const order: string[] = [];
    const tx = { execute: jest.fn(async () => order.push('flag')) } as any;
    const audit = {
      appendAuthSession: jest.fn(async (t: unknown) => {
        expect(t).toBe(tx);
        order.push('audit');
      }),
    } as any;
    const ctx = requesterAuditContext({ role: 'admin', admId: 9, admLoginId: 'adm-few' });
    await setPasswordChangeRequired(tx, 'sales', 4, { audit, ctx });
    expect(order).toEqual(['flag', 'audit']);
    expect(audit.appendAuthSession).toHaveBeenCalledWith(tx, {
      action: 'password.change-required',
      outcome: 'COMMITTED',
      actor: { role: 'ADMIN', refId: 9, refLabel: 'adm-few', orgPath: null, personId: null },
      targetId: 4,
      targetCount: 1,
      changes: { kind: 'sales' },
      requestId: null,
      ip: null,
      userAgent: null,
    });
  });
});

describe('남이 정한 비밀번호 뒤처리 passwordSetByOther (D4 안 1)', () => {
  const run = async (setter: any) => {
    const tx = { execute: jest.fn(async () => undefined) } as any;
    await passwordSetByOther(tx, 'sales', 4, setter);
    return tx.execute.mock.calls.map((c: any[]) => new MySqlDialect().sqlToQuery(c[0]).sql.replace(/\s+/g, ' '));
  };

  it('최고관리자가 정하면 감사 없이 예전 표지를 내리고 세션을 끊는다(V-SQ-6)', async () => {
    const q = await run('super');
    expect(q).toHaveLength(2);
    expect(q[0]).toMatch(/^UPDATE tn_admin_auth_state SET aas_pw_change_required = 0 WHERE aas_kind = \? AND aas_account_id = \?$/);
    expect(q[1]).toMatch(/^UPDATE tn_admin_session SET ses_revoked_at/);
  });

  it('그 밖이 정하면 표지 · 감사 뒤 세션을 끊는다', async () => {
    const audit = { appendAuthSession: jest.fn(async () => undefined) } as any;
    const q = await run({ audit, ctx: requesterAuditContext({ role: 'admin', admId: 9 }) });
    expect(q[0]).toMatch(/^INSERT INTO tn_admin_auth_state .*aas_pw_change_required = 1$/);
    expect(q[1]).toMatch(/^UPDATE tn_admin_session SET ses_revoked_at/);
    expect(audit.appendAuthSession).toHaveBeenCalledTimes(1);
  });
});

describe('recordCodeFailure — 잠근 행 값으로 센다(7.1-6 · D8)', () => {
  const now = 1_000_000;
  const run = async (aasFailCount: number) => {
    const tx = { execute: jest.fn(async () => undefined) } as any;
    const st = { aasKind: 'admin', aasAccountId: 7, aasFailCount, aasLockedUntil: null } as any;
    const entered = await recordCodeFailure(tx, st, now);
    return { entered, params: new MySqlDialect().sqlToQuery(tx.execute.mock.calls[0][0]).params };
  };

  it('5번째 실패면 15분 대기에 들고 실패 수를 0 으로 되돌린다', async () => {
    expect(await run(4)).toEqual({ entered: true, params: [0, now + 900, 'admin', 7] });
  });

  it('그 전에는 실패 수만 하나 올리고 대기는 그대로', async () => {
    expect(await run(0)).toEqual({ entered: false, params: [1, null, 'admin', 7] });
  });
});
