import { Readable } from 'node:stream';

import { HttpStatus, INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import ava, { TestFn } from 'ava';
import Sinon from 'sinon';
import request from 'supertest';

import { AppModule } from '../../src/app.module';
import { CurrentUser } from '../../src/core/auth';
import { AuthService } from '../../src/core/auth/service';
import { DocHistoryManager, DocManager } from '../../src/core/doc';
import { WorkspaceBlobStorage } from '../../src/core/storage';
import { createTestingApp, internalSignIn } from '../utils';

const test = ava as TestFn<{
  u1: CurrentUser;
  db: PrismaClient;
  app: INestApplication;
  storage: Sinon.SinonStubbedInstance<WorkspaceBlobStorage>;
  doc: Sinon.SinonStubbedInstance<DocManager>;
}>;

test.beforeEach(async t => {
  const { app } = await createTestingApp({
    imports: [AppModule],
    tapModule: m => {
      m.overrideProvider(WorkspaceBlobStorage)
        .useValue(Sinon.createStubInstance(WorkspaceBlobStorage))
        .overrideProvider(DocManager)
        .useValue(Sinon.createStubInstance(DocManager))
        .overrideProvider(DocHistoryManager)
        .useValue(Sinon.createStubInstance(DocHistoryManager));
    },
  });

  const auth = app.get(AuthService);
  t.context.u1 = await auth.signUp('u1', 'u1@affine.pro', '1');
  const db = app.get(PrismaClient);

  t.context.db = db;
  t.context.app = app;
  t.context.storage = app.get(WorkspaceBlobStorage);
  t.context.doc = app.get(DocManager);

  await db.workspacePage.create({
    data: {
      workspace: {
        create: {
          id: 'public',
          public: true,
        },
      },
      pageId: 'private',
      public: false,
    },
  });

  await db.workspacePage.create({
    data: {
      workspace: {
        create: {
          id: 'private',
          public: false,
        },
      },
      pageId: 'public',
      public: true,
    },
  });

  await db.workspacePage.create({
    data: {
      workspace: {
        create: {
          id: 'totally-private',
          public: false,
        },
      },
      pageId: 'private',
      public: false,
    },
  });
});

test.afterEach.always(async t => {
  await t.context.app.close();
});

function blob() {
  function stream() {
    return Readable.from(Buffer.from('blob'));
  }

  const init = stream();
  const ret = {
    body: init,
    metadata: {
      contentType: 'text/plain',
      lastModified: new Date(),
      contentLength: 4,
    },
  };

  init.on('end', () => {
    ret.body = stream();
  });

  return ret;
}

// blob
test('should be able to get blob from public workspace', async t => {
  const { app, u1, storage } = t.context;

  // no authenticated user
  storage.get.resolves(blob());
  let res = await request(t.context.app.getHttpServer()).get(
    '/api/workspaces/public/blobs/test'
  );

  t.is(res.status, HttpStatus.OK);
  t.is(res.get('content-type'), 'text/plain');
  t.is(res.text, 'blob');

  // authenticated user
  const cookie = await internalSignIn(app, u1.id);
  res = await request(t.context.app.getHttpServer())
    .get('/api/workspaces/public/blobs/test')
    .set('Cookie', cookie);

  t.is(res.status, HttpStatus.OK);
  t.is(res.get('content-type'), 'text/plain');
  t.is(res.text, 'blob');
});

test('should be able to get private workspace with public pages', async t => {
  const { app, u1, storage } = t.context;

  // no authenticated user
  storage.get.resolves(blob());
  let res = await request(app.getHttpServer()).get(
    '/api/workspaces/private/blobs/test'
  );

  t.is(res.status, HttpStatus.OK);
  t.is(res.get('content-type'), 'text/plain');
  t.is(res.text, 'blob');

  // authenticated user
  const cookie = await internalSignIn(app, u1.id);
  res = await request(app.getHttpServer())
    .get('/api/workspaces/private/blobs/test')
    .set('cookie', cookie);

  t.is(res.status, HttpStatus.OK);
  t.is(res.get('content-type'), 'text/plain');
  t.is(res.text, 'blob');
});

test('should not be able to get private workspace with no public pages', async t => {
  const { app, u1 } = t.context;

  let res = await request(app.getHttpServer()).get(
    '/api/workspaces/totally-private/blobs/test'
  );

  t.is(res.status, HttpStatus.FORBIDDEN);

  res = await request(app.getHttpServer())
    .get('/api/workspaces/totally-private/blobs/test')
    .set('cookie', await internalSignIn(app, u1.id));

  t.is(res.status, HttpStatus.FORBIDDEN);
});

test('should be able to get permission granted workspace', async t => {
  const { app, u1, db, storage } = t.context;

  const cookie = await internalSignIn(app, u1.id);
  await db.workspaceUserPermission.create({
    data: {
      workspaceId: 'totally-private',
      userId: u1.id,
      type: 1,
      accepted: true,
    },
  });

  storage.get.resolves(blob());
  const res = await request(app.getHttpServer())
    .get('/api/workspaces/totally-private/blobs/test')
    .set('Cookie', cookie);

  t.is(res.status, HttpStatus.OK);
  t.is(res.text, 'blob');
});

test('should return 404 if blob not found', async t => {
  const { app, storage } = t.context;

  // @ts-expect-error mock
  storage.get.resolves({ body: null });
  const res = await request(app.getHttpServer()).get(
    '/api/workspaces/public/blobs/test'
  );

  t.is(res.status, HttpStatus.NOT_FOUND);
});

// doc
// NOTE: permission checking of doc api is the same with blob api, skip except one
test('should not be able to get private workspace with private page', async t => {
  const { app, u1 } = t.context;

  let res = await request(app.getHttpServer()).get(
    '/api/workspaces/private/docs/private-page'
  );

  t.is(res.status, HttpStatus.FORBIDDEN);

  res = await request(app.getHttpServer())
    .get('/api/workspaces/private/docs/private-page')
    .set('cookie', await internalSignIn(app, u1.id));

  t.is(res.status, HttpStatus.FORBIDDEN);
});

test('should be able to get doc', async t => {
  const { app, doc } = t.context;

  doc.getBinary.resolves({
    binary: Buffer.from([0, 0]),
    timestamp: Date.now(),
  });

  const res = await request(app.getHttpServer()).get(
    '/api/workspaces/private/docs/public'
  );

  t.is(res.status, HttpStatus.OK);
  t.is(res.get('content-type'), 'application/octet-stream');
  t.deepEqual(res.body, Buffer.from([0, 0]));
});

test('should be able to change page publish mode', async t => {
  const { app, doc, db } = t.context;

  doc.getBinary.resolves({
    binary: Buffer.from([0, 0]),
    timestamp: Date.now(),
  });

  let res = await request(app.getHttpServer()).get(
    '/api/workspaces/private/docs/public'
  );

  t.is(res.status, HttpStatus.OK);
  t.is(res.get('publish-mode'), 'page');

  await db.workspacePage.update({
    where: { workspaceId_pageId: { workspaceId: 'private', pageId: 'public' } },
    data: { mode: 1 },
  });

  res = await request(app.getHttpServer()).get(
    '/api/workspaces/private/docs/public'
  );

  t.is(res.status, HttpStatus.OK);
  t.is(res.get('publish-mode'), 'edgeless');
});
