const assert = require('assert');

class MemoryKV {
  constructor() {
    this.store = new Map();
  }

  async put(key, value = '', options = {}) {
    this.store.set(String(key), {
      value: String(value ?? ''),
      metadata: options?.metadata || null,
    });
  }

  async get(key, options = {}) {
    const entry = this.store.get(String(key));
    if (!entry) return null;
    if (options?.type === 'json') {
      try {
        return JSON.parse(entry.value);
      } catch {
        return null;
      }
    }
    return entry.value;
  }

  async delete(key) {
    this.store.delete(String(key));
  }

  async list({ prefix = '', limit = 1000, cursor } = {}) {
    const keys = [...this.store.entries()]
      .filter(([name]) => String(name).startsWith(String(prefix || '')))
      .map(([name, entry]) => ({
        name,
        metadata: entry.metadata || null,
      }))
      .sort((a, b) => String(a.name).localeCompare(String(b.name)));

    const offset = Math.max(0, Number.parseInt(String(cursor || '0'), 10) || 0);
    const page = keys.slice(offset, offset + limit);
    const nextOffset = offset + limit < keys.length ? String(offset + limit) : undefined;

    return {
      keys: page,
      list_complete: nextOffset == null,
      cursor: nextOffset,
    };
  }

  async getWithMetadata(key) {
    const entry = this.store.get(String(key));
    if (!entry) return null;
    return {
      value: entry.value,
      metadata: entry.metadata || null,
    };
  }
}

class MemoryR2 {
  constructor() {
    this.objects = new Map();
  }

  put(key, bytes) {
    const normalized = bytes instanceof Uint8Array ? bytes : new TextEncoder().encode(String(bytes || ''));
    this.objects.set(String(key), normalized);
  }

  async head(key) {
    const body = this.objects.get(String(key));
    if (!body) return null;
    return { size: body.byteLength };
  }

  async get(key, options = {}) {
    const body = this.objects.get(String(key));
    if (!body) return null;

    if (options?.range) {
      const offset = Number(options.range.offset || 0);
      const length = Number(options.range.length || Math.max(0, body.byteLength - offset));
      const sliced = body.slice(offset, offset + length);
      return {
        body: sliced,
        size: sliced.byteLength,
      };
    }

    return {
      body,
      size: body.byteLength,
    };
  }

  async delete(key) {
    this.objects.delete(String(key));
  }
}

async function sha256Hex(input) {
  const bytes = new TextEncoder().encode(String(input || ''));
  const digest = await crypto.subtle.digest('SHA-256', bytes);
  return Array.from(new Uint8Array(digest))
    .map((byte) => byte.toString(16).padStart(2, '0'))
    .join('');
}

async function parseJson(response) {
  return JSON.parse(await response.text());
}

async function createEnvAndToken(scopes = ['read'], tokenOptions = {}) {
  const { createApiToken } = await import('../functions/utils/api-token.js');
  const env = {
    img_url: new MemoryKV(),
    R2_BUCKET: new MemoryR2(),
  };

  const created = await createApiToken(
    {
      name: 'test-token',
      scopes,
      ...tokenOptions,
    },
    env
  );

  return {
    env,
    token: created.token,
    tokenInfo: created.record,
  };
}

describe('API v1 middleware auth', function () {
  it('rejects missing token', async function () {
    const { onRequest: middleware } = await import('../functions/api/v1/_middleware.js');
    const { env } = await createEnvAndToken(['upload']);

    const request = new Request('https://example.com/api/v1/upload', { method: 'POST' });
    const response = await middleware({
      request,
      env,
      data: {},
      next: () => new Response('ok', { status: 200 }),
      waitUntil: () => {},
    });

    const payload = await parseJson(response);
    assert.strictEqual(response.status, 401);
    assert.strictEqual(payload.success, false);
    assert.strictEqual(payload.error.code, 'TOKEN_INVALID');
  });

  it('rejects token without required scope', async function () {
    const { onRequest: middleware } = await import('../functions/api/v1/_middleware.js');
    const { env, token } = await createEnvAndToken(['read']);

    const request = new Request('https://example.com/api/v1/upload', {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}` },
    });

    const response = await middleware({
      request,
      env,
      data: {},
      next: () => new Response('ok', { status: 200 }),
      waitUntil: () => {},
    });

    const payload = await parseJson(response);
    assert.strictEqual(response.status, 403);
    assert.strictEqual(payload.success, false);
    assert.strictEqual(payload.error.code, 'TOKEN_SCOPE_DENIED');
  });

  it('rejects expired token', async function () {
    const { onRequest: middleware } = await import('../functions/api/v1/_middleware.js');
    const { env, token } = await createEnvAndToken(['read'], {
      expiresAt: Date.now() - 10_000,
    });

    const request = new Request('https://example.com/api/v1/files', {
      method: 'GET',
      headers: { Authorization: `Bearer ${token}` },
    });

    const response = await middleware({
      request,
      env,
      data: {},
      next: () => new Response('ok', { status: 200 }),
      waitUntil: () => {},
    });

    const payload = await parseJson(response);
    assert.strictEqual(response.status, 401);
    assert.strictEqual(payload.success, false);
    assert.strictEqual(payload.error.code, 'TOKEN_EXPIRED');
  });

  it('allows valid token and updates lastUsedAt', async function () {
    const { onRequest: middleware } = await import('../functions/api/v1/_middleware.js');
    const { env, token, tokenInfo } = await createEnvAndToken(['read']);

    const waiters = [];
    const request = new Request('https://example.com/api/v1/files', {
      method: 'GET',
      headers: { Authorization: `Bearer ${token}` },
    });

    let nextCalled = false;
    const response = await middleware({
      request,
      env,
      data: {},
      next: () => {
        nextCalled = true;
        return new Response('ok', { status: 200 });
      },
      waitUntil: (promise) => {
        waiters.push(Promise.resolve(promise));
      },
    });

    await Promise.allSettled(waiters);

    assert.strictEqual(response.status, 200);
    assert.strictEqual(nextCalled, true);

    const tokenRecord = await env.img_url.get(`api_token:${tokenInfo.id}`, { type: 'json' });
    assert.ok(Number(tokenRecord?.lastUsedAt || 0) > 0);
  });
});

describe('API v1 file share limits', function () {
  async function invokeV1FileGet({ env, token, fileId = 'r2:file.bin', query = '' }) {
    const { onRequest: middleware } = await import('../functions/api/v1/_middleware.js');
    const { onRequest: fileRoute } = await import('../functions/api/v1/file/[id].js');

    const encodedId = encodeURIComponent(fileId);
    const requestUrl = `https://example.com/api/v1/file/${encodedId}${query ? `?${query}` : ''}`;
    const request = new Request(requestUrl, {
      method: 'GET',
      headers: { Authorization: `Bearer ${token}` },
    });

    const waiters = [];
    const context = {
      request,
      env,
      params: { id: fileId },
      data: {},
      waitUntil: (promise) => {
        waiters.push(Promise.resolve(promise));
      },
      next: () => fileRoute(context),
    };

    const response = await middleware(context);
    await Promise.allSettled(waiters);
    return response;
  }

  async function createFileScenario(shareMetadata = {}) {
    const { env, token } = await createEnvAndToken(['read']);
    const fileKey = 'r2:file.bin';
    env.R2_BUCKET.put('file.bin', new Uint8Array([1, 2, 3, 4]));
    await env.img_url.put(fileKey, '', {
      metadata: {
        TimeStamp: Date.now(),
        ListType: 'None',
        Label: 'None',
        liked: false,
        fileName: 'file.bin',
        fileSize: 4,
        storageType: 'r2',
        r2Key: 'file.bin',
        ...shareMetadata,
      },
    });
    return { env, token, fileKey };
  }

  it('blocks expired share link', async function () {
    const scenario = await createFileScenario({
      shareExpiresAt: Date.now() - 60_000,
    });

    const response = await invokeV1FileGet(scenario);
    const payload = await parseJson(response);

    assert.strictEqual(response.status, 410);
    assert.strictEqual(payload.success, false);
    assert.strictEqual(payload.error.code, 'FILE_LINK_EXPIRED');
  });

  it('blocks share when max downloads reached', async function () {
    const scenario = await createFileScenario({
      shareMaxDownloads: 1,
      shareDownloadCount: 1,
    });

    const response = await invokeV1FileGet(scenario);
    const payload = await parseJson(response);

    assert.strictEqual(response.status, 410);
    assert.strictEqual(payload.success, false);
    assert.strictEqual(payload.error.code, 'FILE_LINK_EXPIRED');
  });

  it('requires password for protected share', async function () {
    const salt = 'salt123';
    const hash = await sha256Hex(`${salt}:secret-pass`);
    const scenario = await createFileScenario({
      sharePasswordSalt: salt,
      sharePasswordHash: hash,
    });

    const response = await invokeV1FileGet(scenario);
    const payload = await parseJson(response);

    assert.strictEqual(response.status, 401);
    assert.strictEqual(payload.success, false);
    assert.strictEqual(payload.error.code, 'FILE_PASSWORD_REQUIRED');
  });

  it('accepts correct password and increments download counter', async function () {
    const salt = 'salt-ok';
    const password = 'correct-pass';
    const hash = await sha256Hex(`${salt}:${password}`);
    const scenario = await createFileScenario({
      shareMaxDownloads: 5,
      shareDownloadCount: 0,
      sharePasswordSalt: salt,
      sharePasswordHash: hash,
    });

    const response = await invokeV1FileGet({
      ...scenario,
      query: `password=${encodeURIComponent(password)}`,
    });

    assert.strictEqual(response.status, 200);
    const record = await scenario.env.img_url.getWithMetadata(scenario.fileKey);
    assert.strictEqual(Number(record?.metadata?.shareDownloadCount || 0), 1);
  });
});
