import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { createAssetService, validateUploadRequest } from './mindspace-assets.mjs';
import { DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';

function createMockPool(state) {
  return {
    async getConnection() {
      return {
        async beginTransaction() {},
        async commit() {},
        async rollback() {},
        release() {},
        async query(sql, params = []) {
          if (sql.includes('FROM h5_space_categories') && sql.includes('FOR UPDATE')) {
            const category = state.categories.find(
              (item) => item.id === params[0] && item.user_id === params[1],
            );
            return [category ? [category] : []];
          }
          if (sql.includes('FROM h5_user_spaces') && sql.includes('FOR UPDATE')) {
            const space = state.spaces.find(
              (item) => item.id === params[0] && item.user_id === params[1],
            );
            return [space ? [space] : []];
          }
          if (sql.includes('INSERT INTO h5_upload_sessions')) {
            state.uploads.push({
              id: params[0],
              user_id: params[1],
              space_id: params[2],
              category_id: params[3],
              filename: params[4],
              expected_size: params[5],
              reserved_bytes: params[7],
              temporary_storage_key: params[8],
              status: 'reserved',
              expires_at: params[9],
            });
            return [[]];
          }
          if (sql.includes('UPDATE h5_user_spaces') && sql.includes('reserved_bytes = reserved_bytes +')) {
            const space = state.spaces.find((item) => item.id === params[2]);
            space.reserved_bytes += params[0];
            return [[]];
          }
          if (sql.includes('FROM h5_upload_sessions') && sql.includes('WHERE id = ?')) {
            const upload = state.uploads.find(
              (item) => item.id === params[0] && item.user_id === params[1],
            );
            return [upload ? [upload] : []];
          }
          if (sql.includes('UPDATE h5_upload_sessions') && sql.includes("status = 'uploaded'")) {
            const upload = state.uploads.find(
              (item) => item.id === params[3] && item.user_id === params[4],
            );
            upload.actual_size = params[0];
            upload.detected_mime_type = params[1];
            upload.checksum = params[2];
            upload.status = 'uploaded';
            return [[]];
          }
          if (sql.includes('FROM h5_upload_sessions u') && sql.includes('FOR UPDATE')) {
            const upload = state.uploads.find(
              (item) => item.id === params[0] && item.user_id === params[1],
            );
            if (!upload) return [[]];
            const category = state.categories.find((item) => item.id === upload.category_id);
            return [[{ ...upload, category_code: category.category_code }]];
          }
          if (sql.includes('INSERT INTO h5_assets')) {
            state.assets.push({
              id: params[0],
              user_id: params[1],
              status: params[13],
              risk_level: params[11],
              current_version_id: params[8],
              original_filename: params[6],
              mime_type: params[5],
              size_bytes: params[9],
            });
            return [[]];
          }
          if (sql.includes('INSERT INTO h5_asset_versions')) {
            state.versions.push({
              id: params[0],
              asset_id: params[1],
              storage_key: params[2],
              scan_status: params[8],
            });
            return [[]];
          }
          if (sql.includes('UPDATE h5_user_spaces') && sql.includes('used_bytes = used_bytes +')) {
            const space = state.spaces.find((item) => item.id === params[3]);
            space.reserved_bytes -= params[0];
            space.used_bytes += params[1];
            return [[]];
          }
          if (sql.includes("status = 'completed'")) {
            const upload = state.uploads.find(
              (item) => item.id === params[2] && item.user_id === params[3],
            );
            upload.status = 'completed';
            upload.completed_asset_id = params[1];
            return [[]];
          }
          if (sql.includes('JOIN h5_asset_versions v')) {
            const asset = state.assets.find(
              (item) => item.id === params[0] && item.user_id === params[1],
            );
            if (!asset) return [[]];
            const version = state.versions.find((item) => item.id === asset.current_version_id);
            const category = state.categories.find((item) => item.id === asset.category_id);
            return [[
              {
                ...asset,
                category_code: category?.category_code ?? 'oa',
                storage_key: version.storage_key,
                scan_status: version.scan_status,
                category_id: category?.id,
                display_name: asset.original_filename,
                asset_type: 'file',
                visibility: 'private',
                source_type: 'upload',
                checksum: 'abc',
                created_at: 1,
                updated_at: 1,
              },
            ]];
          }
          if (sql.includes("status IN ('reserved', 'uploaded') AND expires_at")) {
            return [state.uploads.filter((item) => item.expires_at <= params[0])];
          }
          return [[]];
        },
      };
    },
    async query(sql, params = []) {
      if (sql.includes('FROM h5_upload_sessions') && sql.includes('WHERE id = ?')) {
        const upload = state.uploads.find(
          (item) => item.id === params[0] && item.user_id === params[1],
        );
        return [upload ? [upload] : []];
      }
      if (sql.includes('UPDATE h5_upload_sessions') && sql.includes("status = 'uploaded'")) {
        const upload = state.uploads.find(
          (item) => item.id === params[3] && item.user_id === params[4],
        );
        upload.actual_size = params[0];
        upload.detected_mime_type = params[1];
        upload.checksum = params[2];
        upload.status = 'uploaded';
        return [[]];
      }
      return [[]];
    },
  };
}

test('validateUploadRequest rejects traversal filenames', () => {
  assert.throws(
    () => validateUploadRequest({ filename: '../secret.pdf', sizeBytes: 10 }),
    (error) => error.code === 'invalid_filename',
  );
});

test('validateUploadRequest allows 5MB uploads by default', () => {
  assert.doesNotThrow(() =>
    validateUploadRequest({
      filename: 'handbook.pdf',
      sizeBytes: DEFAULT_MAX_FILE_BYTES,
    }),
  );
  assert.throws(
    () =>
      validateUploadRequest({
        filename: 'handbook.pdf',
        sizeBytes: DEFAULT_MAX_FILE_BYTES + 1,
      }),
    (error) => error.code === 'file_too_large',
  );
});

test('completeUpload marks safe files ready and blocks script payloads', async () => {
  const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-'));
  const state = {
    categories: [
      { id: 'cat-1', user_id: 'user-1', space_id: 'space-1', category_code: 'oa' },
      { id: 'cat-2', user_id: 'user-2', space_id: 'space-2', category_code: 'oa' },
    ],
    spaces: [
      {
        id: 'space-1',
        user_id: 'user-1',
        quota_bytes: 5 * 1024 * 1024,
        used_bytes: 0,
        reserved_bytes: 0,
        status: 'active',
      },
    ],
    uploads: [],
    assets: [],
    versions: [],
  };
  let nextId = 0;
  const service = createAssetService(createMockPool(state), {
    storageRoot,
    idFactory: () => `id-${++nextId}`,
  });

  const payload = Buffer.from('<script>alert(1)</script>');
  const upload = await service.createUpload('user-1', {
    categoryId: 'cat-1',
    filename: 'note.txt',
    sizeBytes: payload.length,
  });
  assert.match(state.uploads[0].temporary_storage_key, /^users\/user-1\/temp\//);
  await service.writeUploadContent('user-1', upload.id, payload);
  const asset = await service.completeUpload('user-1', upload.id);
  assert.equal(asset.status, 'quarantined');
  assert.equal(asset.scanStatus, 'blocked');
});

test('completeUpload publishes public images to the public temp image library', async () => {
  const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-'));
  const h5Root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-h5-'));
  const state = {
    categories: [
      { id: 'cat-public', user_id: 'user-1', space_id: 'space-1', category_code: 'public' },
    ],
    spaces: [
      {
        id: 'space-1',
        user_id: 'user-1',
        quota_bytes: 5 * 1024 * 1024,
        used_bytes: 0,
        reserved_bytes: 0,
        status: 'active',
      },
    ],
    uploads: [],
    assets: [],
    versions: [],
  };
  let nextId = 0;
  const service = createAssetService(createMockPool(state), {
    storageRoot,
    h5Root,
    idFactory: () => `id-${++nextId}`,
  });
  const png = Buffer.from(
    'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=',
    'base64',
  );

  const upload = await service.createUpload('user-1', {
    categoryId: 'cat-public',
    filename: 'look.png',
    sizeBytes: png.length,
  });
  await service.writeUploadContent('user-1', upload.id, png);
  const asset = await service.completeUpload('user-1', upload.id);

  assert.equal(asset.status, 'ready');
  assert.match(asset.publicUrl, /^https:\/\/g2\.tkmind\.cn\/MindSpace\/user-1\/public\/\.tmp-images\/id-2\.png$/);
  assert.equal(state.versions[0].storage_key, 'users/user-1/public/.tmp-images/id-2.png');
  await assert.doesNotReject(() =>
    fs.stat(path.join(storageRoot, 'users/user-1/public/.tmp-images/id-2.png')),
  );
  await assert.doesNotReject(() =>
    fs.stat(path.join(h5Root, 'MindSpace/user-1/public/.tmp-images/id-2.png')),
  );
});

test('createUpload rejects another users category id', async () => {
  const state = {
    categories: [
      { id: 'cat-1', user_id: 'user-1', space_id: 'space-1', category_code: 'oa' },
      { id: 'cat-2', user_id: 'user-2', space_id: 'space-2', category_code: 'oa' },
    ],
    spaces: [
      {
        id: 'space-1',
        user_id: 'user-1',
        quota_bytes: 5 * 1024 * 1024,
        used_bytes: 0,
        reserved_bytes: 0,
        status: 'active',
      },
    ],
    uploads: [],
    assets: [],
    versions: [],
  };
  const service = createAssetService(createMockPool(state), {
    storageRoot: await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-')),
  });

  await assert.rejects(
    () =>
      service.createUpload('user-1', {
        categoryId: 'cat-2',
        filename: 'note.txt',
        sizeBytes: 4,
      }),
    (error) => error.code === 'category_not_found',
  );
});
