import assert from 'node:assert/strict';
import test from 'node:test';
import { createExperienceService } from './experience-service.mjs';

// Minimal in-memory pool covering the SQL shapes experience-service issues:
// INSERT, scope-filtered SELECT, and the use_count bump UPDATE.
function createMockPool() {
  const rows = [];
  const query = async (sql, params = []) => {
    if (sql.includes('INSERT INTO h5_experience')) {
      const [
        id,
        scope,
        kind,
        title,
        body,
        tagsJson,
        sourceSessionId,
        sourceUserId,
        createdAt,
        updatedAt,
      ] = params;
      rows.push({
        id,
        scope,
        kind,
        title,
        body,
        tags_json: tagsJson,
        source_session_id: sourceSessionId,
        source_user_id: sourceUserId,
        use_count: 0,
        created_at: createdAt,
        updated_at: updatedAt,
      });
      return [{ affectedRows: 1 }];
    }
    if (sql.includes('FROM h5_experience') && sql.includes('WHERE scope = ?')) {
      const [scope] = params;
      const matched = rows
        .filter((r) => r.scope === scope)
        .sort((a, b) => b.updated_at - a.updated_at);
      return [matched.map((r) => ({ ...r }))];
    }
    if (sql.includes('UPDATE h5_experience SET use_count')) {
      for (const id of params) {
        const row = rows.find((r) => r.id === id);
        if (row) row.use_count += 1;
      }
      return [{ affectedRows: params.length }];
    }
    throw new Error(`unexpected SQL: ${sql}`);
  };
  return { pool: { query }, rows };
}

test('record persists an experience with normalized fields', async () => {
  const { pool, rows } = createMockPool();
  const svc = createExperienceService(pool, { now: () => 1000 });
  const saved = await svc.record({
    title: 'Headscale 部署',
    body: '在 103 上用固定公网 IP 部署 headscale，注意防火墙放行 41641/udp。',
    tags: ['headscale', 'headscale', '  网络  ', ''],
  });
  assert.equal(rows.length, 1);
  assert.equal(saved.scope, 'global');
  assert.equal(saved.kind, 'lesson');
  assert.deepEqual(saved.tags, ['headscale', '网络']);
  assert.equal(saved.useCount, 0);
});

test('record rejects empty title or body', async () => {
  const { pool } = createMockPool();
  const svc = createExperienceService(pool);
  await assert.rejects(() => svc.record({ title: '', body: 'x' }), /标题不能为空/);
  await assert.rejects(() => svc.record({ title: 'x', body: '' }), /内容不能为空/);
});

test('search ranks keyword matches and ignores empty queries', async () => {
  const { pool } = createMockPool();
  const svc = createExperienceService(pool, { now: () => 1_000_000 });
  await svc.record({ title: 'Headscale 部署', body: 'headscale 防火墙 udp' });
  await svc.record({ title: '无关经验', body: '这条跟查询完全无关的内容' });

  assert.deepEqual(await svc.search('   '), []);

  const hits = await svc.search('headscale 部署');
  assert.equal(hits.length, 1);
  assert.equal(hits[0].title, 'Headscale 部署');
});

test('search applies recency decay so newer wins on equal keyword score', async () => {
  const { pool } = createMockPool();
  let clock = 0;
  const svc = createExperienceService(pool, {
    now: () => clock,
    recencyHalfLifeMs: 1000,
  });
  clock = 0;
  await svc.record({ title: '部署指南 A', body: 'deploy 部署 指南' });
  clock = 10_000; // 10 half-lives newer
  await svc.record({ title: '部署指南 B', body: 'deploy 部署 指南' });
  clock = 10_000;
  const hits = await svc.search('部署 deploy 指南', { limit: 2 });
  assert.equal(hits[0].title, '部署指南 B');
});

test('search bumps use_count for returned rows', async () => {
  const { pool, rows } = createMockPool();
  const svc = createExperienceService(pool, { now: () => 5 });
  await svc.record({ title: 'Caddy 灰度', body: 'caddy weighted_round_robin 分流' });
  await svc.search('caddy 灰度');
  assert.equal(rows[0].use_count, 1);
});
