import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { Algorithm as Argon2Algorithm, hashRawSync as argon2HashRawSync } from '@node-rs/argon2';
import { createUserAuth } from './user-auth.mjs';

function hashPasswordPbkdf2(password, salt) {
  return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
}

function hashPasswordArgon2id(password, salt) {
  return argon2HashRawSync(password, {
    salt: Buffer.from(salt, 'hex'),
    parallelism: 1,
    outputLen: 32,
    memoryCost: 64 * 1024,
    timeCost: 3,
    algorithm: Argon2Algorithm.Argon2id,
  }).toString('hex');
}

function createAuthPool(userRow) {
  const sessions = [];
  return {
    async query(sql, params = []) {
      if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.username = ?')) {
        return userRow ? [[userRow]] : [[]];
      }
      if (sql.includes('UPDATE h5_users') && sql.includes('password_algorithm')) {
        userRow.salt = params[0];
        userRow.password_hash = params[1];
        userRow.password_algorithm = params[2];
        return [[]];
      }
      if (sql.includes('INSERT INTO h5_login_sessions')) {
        sessions.push({
          token_hash: params[2],
          user_id: params[1],
          expires_at: params[3],
          revoked_at: null,
        });
        return [[]];
      }
      if (sql.includes('FROM h5_login_sessions s')) {
        const row = sessions.find((item) => item.token_hash === params[0] && !item.revoked_at);
        if (!row) return [[]];
        return [[
          {
            user_id: row.user_id,
            expires_at: row.expires_at,
            role: userRow.role,
            status: userRow.status,
          },
        ]];
      }
      if (sql.includes('UPDATE h5_login_sessions SET expires_at')) {
        const row = sessions.find((item) => item.token_hash === params[1]);
        if (row) row.expires_at = params[0];
        return [[]];
      }
      if (sql.includes('UPDATE h5_login_sessions SET revoked_at')) {
        for (const row of sessions) {
          if (row.user_id === params[1]) row.revoked_at = params[0];
        }
        return [[]];
      }
      if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) {
        return [[userRow]];
      }
      return [[]];
    },
  };
}

function createAdminLayoutPool(adminRow) {
  return {
    async query(sql, params = []) {
      if (sql.includes('SELECT id FROM h5_users WHERE username = ? LIMIT 1')) {
        return adminRow ? [[{ id: adminRow.id }]] : [[]];
      }
      if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) {
        return [[adminRow]];
      }
      if (sql.includes('FROM h5_assets a') && sql.includes('JOIN h5_space_categories')) {
        return [[]];
      }
      if (sql.includes('UPDATE h5_users SET workspace_root = ?')) {
        adminRow.workspace_root = params[0];
        return [[]];
      }
      if (sql.includes('DELETE FROM h5_user_path_grants')) return [[]];
      if (sql.includes('INSERT INTO h5_user_path_grants')) return [[]];
      if (sql.includes('UPDATE h5_user_wallets SET balance_cents')) return [[]];
      if (sql.includes('FROM h5_login_sessions')) return [[]];
      return [[]];
    },
  };
}

test('login rate limits repeated failures', async () => {
  const auth = createUserAuth(createAuthPool(null), {
    loginMaxFailures: 2,
    loginFailureWindowMs: 60_000,
    persistSessions: false,
  });

  assert.equal((await auth.login({ username: 'john', password: 'bad', ip: '1.1.1.1' })).ok, false);
  assert.equal((await auth.login({ username: 'john', password: 'bad', ip: '1.1.1.1' })).ok, false);
  const blocked = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' });
  assert.equal(blocked.ok, false);
  assert.ok(blocked.retryAfterMs > 0);
});

test('verify rejects disabled users after login', async () => {
  const salt = '00112233445566778899aabbccddeeff';
  const userRow = {
    id: 'user-1',
    username: 'john',
    slug: 'john',
    email: 'john@example.com',
    display_name: 'John',
    role: 'user',
    status: 'active',
    plan_type: 'free',
    workspace_root: '/tmp/john',
    salt,
    password_hash: hashPasswordArgon2id('secret', salt),
    password_algorithm: 'argon2id',
    balance_cents: 100,
    tokens_used: 0,
  };
  const auth = createUserAuth(createAuthPool(userRow), { persistSessions: true });
  const login = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' });
  assert.equal(login.ok, true);
  userRow.status = 'disabled';
  assert.equal(await auth.verify(login.token), null);
});

test('resetPassword updates hash when username and email match', async () => {
  const salt = '11112222333344445555666677778888';
  const userRow = {
    id: 'user-1',
    username: 'john',
    email: 'john@example.com',
    status: 'active',
    salt,
    password_hash: hashPasswordPbkdf2('old-secret', salt),
    password_algorithm: 'pbkdf2-sha512',
  };
  const pool = {
    async query(sql, params = []) {
      if (sql.includes('SELECT id, email, status FROM h5_users WHERE username = ?')) {
        return userRow.username === params[0] ? [[userRow]] : [[]];
      }
      if (sql.includes('UPDATE h5_users SET salt = ?')) {
        userRow.salt = params[0];
        userRow.password_hash = params[1];
        userRow.password_algorithm = params[2];
        return [[]];
      }
      if (sql.includes('UPDATE h5_login_sessions SET revoked_at')) {
        return [[]];
      }
      return [[]];
    },
  };
  const auth = createUserAuth(pool, { persistSessions: false });
  const ok = await auth.resetPassword({
    username: 'john',
    email: 'john@example.com',
    password: 'new-secret',
  });
  assert.equal(ok.ok, true);
  assert.equal(userRow.password_algorithm, 'argon2id');
  assert.equal(userRow.password_hash, hashPasswordArgon2id('new-secret', userRow.salt));
  const bad = await auth.resetPassword({
    username: 'john',
    email: 'wrong@example.com',
    password: 'another',
  });
  assert.equal(bad.ok, false);
});

test('login migrates pbkdf2 passwords to argon2id after successful verification', async () => {
  const salt = '9999aaaabbbbccccddddeeeeffff0000';
  const userRow = {
    id: 'user-1',
    username: 'john',
    slug: 'john',
    email: 'john@example.com',
    display_name: 'John',
    role: 'user',
    status: 'active',
    plan_type: 'free',
    workspace_root: '/tmp/john',
    salt,
    password_hash: hashPasswordPbkdf2('secret', salt),
    password_algorithm: 'pbkdf2-sha512',
    balance_cents: 100,
    tokens_used: 0,
  };
  const auth = createUserAuth(createAuthPool(userRow), { persistSessions: false });
  const login = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' });
  assert.equal(login.ok, true);
  assert.equal(userRow.password_algorithm, 'argon2id');
  assert.equal(userRow.password_hash, hashPasswordArgon2id('secret', userRow.salt));
});

test('admin users get a real MindSpace publish layout', async () => {
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-admin-layout-'));
  const adminRow = {
    id: 'admin-1',
    username: 'admin',
    slug: 'admin',
    email: 'admin@example.com',
    display_name: '管理员',
    role: 'admin',
    status: 'active',
    plan_type: 'free',
    workspace_root: path.join(root, 'legacy-admin-root'),
    balance_cents: 0,
    tokens_used: 0,
  };
  const auth = createUserAuth(createAdminLayoutPool(adminRow), {
    h5Root: root,
    env: { H5_PUBLIC_BASE_URL: 'https://example.com' },
    persistSessions: false,
  });

  const layout = await auth.getUserPublishLayout(adminRow.id);
  assert.ok(layout, 'admin should receive a publish layout');
  assert.equal(layout.slug, adminRow.id);
  assert.equal(layout.username, 'admin');
  assert.equal(layout.publishDir, path.join(root, 'MindSpace', adminRow.id));
  assert.equal(layout.publicUrl, `https://example.com/MindSpace/${adminRow.id}/`);

  const workingDir = await auth.resolveWorkingDir(adminRow.id);
  assert.equal(workingDir, layout.publishDir);
  assert.equal(adminRow.workspace_root, layout.publishDir);
});

test('ensureAdminUser repairs existing admin workspace without password env', async () => {
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-admin-repair-'));
  const adminRow = {
    id: 'admin-2',
    username: 'admin',
    slug: 'admin',
    email: 'admin@example.com',
    display_name: '管理员',
    role: 'admin',
    status: 'active',
    plan_type: 'free',
    workspace_root: path.join(root, 'legacy-admin-root'),
    balance_cents: 0,
    tokens_used: 0,
  };
  const auth = createUserAuth(createAdminLayoutPool(adminRow), {
    h5Root: root,
    env: { H5_PUBLIC_BASE_URL: 'https://example.com' },
    persistSessions: false,
  });

  await auth.ensureAdminUser();
  assert.equal(adminRow.workspace_root, path.join(root, 'MindSpace', adminRow.id));
});

test('admin capabilities include granted platform skills', async () => {
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-admin-skills-'));
  await fs.mkdir(path.join(root, 'skills', 'product-campaign-page'), { recursive: true });
  await fs.writeFile(
    path.join(root, 'skills', 'product-campaign-page', 'SKILL.md'),
    '---\nname: product-campaign-page\ndescription: 商品宣传 / 活动页技能\n---\n',
  );
  const adminRow = {
    id: 'admin-3',
    username: 'admin',
    slug: 'admin',
    email: 'admin@example.com',
    display_name: '管理员',
    role: 'admin',
    status: 'active',
    plan_type: 'free',
    workspace_root: '/tmp/admin',
    balance_cents: 0,
    tokens_used: 0,
  };
  const auth = createUserAuth(createAdminLayoutPool(adminRow), {
    h5Root: root,
    persistSessions: false,
  });

  const resolved = await auth.resolveUserCapabilities(adminRow);

  assert.equal(resolved.unrestricted, true);
  assert.ok(resolved.grantedSkills.includes('product-campaign-page'));
});

test('recharge sends notifier payload after successful self recharge', async () => {
  const userRow = {
    id: 'user-1',
    username: 'john',
    slug: 'john',
    email: 'john@example.com',
    display_name: 'John',
    role: 'user',
    status: 'active',
    plan_type: 'free',
    workspace_root: '/tmp/john',
    balance_cents: 1200,
    tokens_used: 0,
    spent_cents: 0,
  };
  const queries = [];
  const notifierCalls = [];
  const pool = {
    async query(sql) {
      queries.push(sql);
      if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) {
        return [[userRow]];
      }
      throw new Error(`unexpected pool query: ${sql}`);
    },
    async getConnection() {
      return {
        async beginTransaction() {},
        async commit() {},
        async rollback() {},
        release() {},
        async query(sql) {
          queries.push(sql);
          if (sql.includes('INSERT INTO h5_user_wallets')) return [{ affectedRows: 1 }, []];
          if (sql.includes('INSERT INTO h5_billing_ledger')) return [{ affectedRows: 1 }, []];
          if (sql.includes('INSERT INTO h5_user_notifications')) return [{ affectedRows: 1 }, []];
          throw new Error(`unexpected connection query: ${sql}`);
        },
      };
    },
  };
  const auth = createUserAuth(pool, {
    persistSessions: false,
    onRechargeNotification: async (payload) => {
      notifierCalls.push(payload);
    },
  });

  const result = await auth.recharge('user-1', 2500, null, '用户自助充值', {
    paymentOrderId: 'order-1',
  });
  assert.equal(result.ok, true);
  assert.equal(notifierCalls.length, 1);
  assert.equal(notifierCalls[0].notificationType, 'self_recharge');
  assert.equal(notifierCalls[0].title, '充值成功');
  assert.equal(notifierCalls[0].amountCents, 2500);
  assert.equal(notifierCalls[0].paymentOrderId, 'order-1');
  assert.ok(queries.some((sql) => sql.includes('INSERT INTO h5_user_notifications')));
});

test('purchaseSpaceQuota deducts balance and increases quota', async () => {
  const userRow = {
    id: 'user-2',
    username: 'alice',
    slug: 'alice',
    email: 'alice@example.com',
    display_name: 'Alice',
    role: 'user',
    status: 'active',
    plan_type: 'free',
    workspace_root: '/tmp/alice',
    balance_cents: 5000,
    tokens_used: 0,
    spent_cents: 0,
    quota_bytes: 5 * 1024 * 1024,
    used_bytes: 1024,
    reserved_bytes: 2048,
  };
  const spaceRow = {
    id: 'space-1',
    quota_bytes: 5 * 1024 * 1024,
    used_bytes: 1024,
    reserved_bytes: 2048,
  };
  const pool = {
    async query(sql) {
      if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) {
        return [[userRow]];
      }
      if (sql.includes('FROM h5_user_spaces') && sql.includes('WHERE user_id = ?')) {
        return [[spaceRow]];
      }
      throw new Error(`unexpected pool query: ${sql}`);
    },
    async getConnection() {
      return {
        async beginTransaction() {},
        async commit() {},
        async rollback() {},
        release() {},
        async query(sql, params) {
          if (sql.includes('SELECT id, quota_bytes, used_bytes, reserved_bytes')) {
            return [[spaceRow]];
          }
          if (sql.includes('SELECT balance_cents')) {
            return [[{ balance_cents: userRow.balance_cents }]];
          }
          if (sql.includes('UPDATE h5_user_wallets')) {
            userRow.balance_cents -= params[0];
            return [{ affectedRows: 1 }, []];
          }
          if (sql.includes('UPDATE h5_user_spaces')) {
            spaceRow.quota_bytes += params[0];
            userRow.quota_bytes = spaceRow.quota_bytes;
            return [{ affectedRows: 1 }, []];
          }
          if (sql.includes('INSERT INTO h5_billing_ledger')) return [{ affectedRows: 1 }, []];
          if (sql.includes('INSERT INTO h5_user_notifications')) return [{ affectedRows: 1 }, []];
          throw new Error(`unexpected connection query: ${sql}`);
        },
      };
    },
  };
  const auth = createUserAuth(pool, { persistSessions: false });

  const result = await auth.purchaseSpaceQuota('user-2', 10);

  assert.equal(result.ok, true);
  assert.equal(result.balanceCents, 3000);
  assert.equal(result.quota.quotaBytes, 15 * 1024 * 1024);
  assert.equal(result.quota.availableBytes, 15 * 1024 * 1024 - 1024 - 2048);
});

test('updateUser rejects quota smaller than occupied bytes', async () => {
  const userRow = {
    id: 'user-3',
    username: 'bob',
    slug: 'bob',
    email: 'bob@example.com',
    display_name: 'Bob',
    role: 'user',
    status: 'active',
    plan_type: 'free',
    workspace_root: '/tmp/bob',
    balance_cents: 1000,
    tokens_used: 0,
    spent_cents: 0,
    quota_bytes: 5 * 1024 * 1024,
    used_bytes: 4 * 1024 * 1024,
    reserved_bytes: 2 * 1024 * 1024,
  };
  const pool = {
    async query(sql) {
      if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) {
        return [[userRow]];
      }
      if (sql.includes('FROM h5_user_spaces') && sql.includes('WHERE user_id = ?')) {
        return [[{
          id: 'space-2',
          quota_bytes: userRow.quota_bytes,
          used_bytes: userRow.used_bytes,
          reserved_bytes: userRow.reserved_bytes,
        }]];
      }
      throw new Error(`unexpected pool query: ${sql}`);
    },
  };
  const auth = createUserAuth(pool, { persistSessions: false });

  const result = await auth.updateUser('user-3', {
    spaceQuotaBytes: 5 * 1024 * 1024,
  });

  assert.equal(result.ok, false);
  assert.match(result.message, /空间不能小于已使用容量/);
});

test('updateUser persists a larger space quota', async () => {
  const userRow = {
    id: 'user-4',
    username: 'carol',
    slug: 'carol',
    email: 'carol@example.com',
    display_name: 'Carol',
    role: 'user',
    status: 'active',
    plan_type: 'free',
    workspace_root: '/tmp/carol',
    balance_cents: 1000,
    tokens_used: 0,
    spent_cents: 0,
    quota_bytes: 5 * 1024 * 1024,
    used_bytes: 1 * 1024 * 1024,
    reserved_bytes: 0,
  };
  const pool = {
    async query(sql, params = []) {
      if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) {
        return [[userRow]];
      }
      if (sql.includes('FROM h5_user_spaces') && sql.includes('WHERE user_id = ?')) {
        return [[{
          id: 'space-4',
          quota_bytes: userRow.quota_bytes,
          used_bytes: userRow.used_bytes,
          reserved_bytes: userRow.reserved_bytes,
        }]];
      }
      if (sql.includes('UPDATE h5_user_spaces')) {
        userRow.quota_bytes = params[0];
        return [{ affectedRows: 1 }, []];
      }
      throw new Error(`unexpected pool query: ${sql}`);
    },
  };
  const auth = createUserAuth(pool, { persistSessions: false });

  const result = await auth.updateUser('user-4', {
    spaceQuotaBytes: 20 * 1024 * 1024,
  });

  assert.equal(result.ok, true);
  assert.equal(result.user.spaceQuotaBytes, 20 * 1024 * 1024);
  assert.equal(result.user.spaceAvailableBytes, 19 * 1024 * 1024);
});

test('upsertWechatAgentRoute uses atomic insert-on-duplicate semantics', async () => {
  const calls = [];
  const pool = {
    async query(sql, params = []) {
      calls.push([sql, params]);
      if (sql.includes('INSERT INTO h5_wechat_agent_routes')) return [[]];
      if (sql.includes('FROM h5_wechat_agent_routes')) {
        return [[{ id: 'route-1', user_id: 'user-1', agent_session_id: 'session-2', status: 'active', created_at: 1, updated_at: 2 }]];
      }
      return [[]];
    },
  };
  const auth = createUserAuth(pool, { persistSessions: false });

  const routeId = await auth.upsertWechatAgentRoute({
    userId: 'user-1',
    appId: 'wx123',
    openid: 'openid-1',
    agentSessionId: 'session-2',
  });

  assert.equal(routeId, 'route-1');
  assert.match(calls[0][0], /ON DUPLICATE KEY UPDATE/);
  assert.equal(calls.some(([sql]) => /UPDATE h5_wechat_agent_routes/.test(sql)), false);
});
