import { Insertable, Kysely } from 'kysely';
import { randomBytes, randomUUID } from 'node:crypto';
import { Writable } from 'node:stream';
import { Assets, DB, Sessions, Users } from 'src/db';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetType } from 'src/enum';
import { AlbumRepository } from 'src/repositories/album.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SyncRepository } from 'src/repositories/sync.repository';
import { UserRepository } from 'src/repositories/user.repository';

class CustomWritable extends Writable {
  private data = '';

  _write(chunk: any, encoding: string, callback: () => void) {
    this.data += chunk.toString();
    callback();
  }

  getResponse() {
    const result = this.data;
    return result
      .split('\n')
      .filter((x) => x.length > 0)
      .map((x) => JSON.parse(x));
  }
}

type Asset = Insertable<Assets>;
type User = Partial<Insertable<Users>>;
type Session = Omit<Insertable<Sessions>, 'token'> & { token?: string };

export const newUuid = () => randomUUID() as string;

export class TestFactory {
  private assets: Asset[] = [];
  private sessions: Session[] = [];
  private users: User[] = [];

  private constructor(private context: TestContext) {}

  static create(context: TestContext) {
    return new TestFactory(context);
  }

  static stream() {
    return new CustomWritable();
  }

  static asset(asset: Asset) {
    const assetId = asset.id || newUuid();
    const defaults: Insertable<Assets> = {
      deviceAssetId: '',
      deviceId: '',
      originalFileName: '',
      checksum: randomBytes(32),
      type: AssetType.IMAGE,
      originalPath: '/path/to/something.jpg',
      ownerId: '@immich.cloud',
      isVisible: true,
    };

    return {
      ...defaults,
      ...asset,
      id: assetId,
    };
  }

  static auth(auth: { user: User; session?: Session }) {
    return auth as AuthDto;
  }

  static user(user: User = {}) {
    const userId = user.id || newUuid();
    const defaults: Insertable<Users> = {
      email: `${userId}@immich.cloud`,
      name: `User ${userId}`,
      deletedAt: null,
    };

    return {
      ...defaults,
      ...user,
      id: userId,
    };
  }

  static session(session: Session) {
    const id = session.id || newUuid();
    const defaults = {
      token: randomBytes(36).toString('base64url'),
    };

    return {
      ...defaults,
      ...session,
      id,
    };
  }

  withAsset(asset: Asset) {
    this.assets.push(asset);
    return this;
  }

  withSession(session: Session) {
    this.sessions.push(session);
    return this;
  }

  withUser(user: User = {}) {
    this.users.push(user);
    return this;
  }

  async create() {
    for (const asset of this.assets) {
      await this.context.createAsset(asset);
    }

    for (const user of this.users) {
      await this.context.createUser(user);
    }

    for (const session of this.sessions) {
      await this.context.createSession(session);
    }

    return this.context;
  }
}

export class TestContext {
  userRepository: UserRepository;
  assetRepository: AssetRepository;
  albumRepository: AlbumRepository;
  sessionRepository: SessionRepository;
  syncRepository: SyncRepository;

  private constructor(private db: Kysely<DB>) {
    this.userRepository = new UserRepository(this.db);
    this.assetRepository = new AssetRepository(this.db);
    this.albumRepository = new AlbumRepository(this.db);
    this.sessionRepository = new SessionRepository(this.db);
    this.syncRepository = new SyncRepository(this.db);
  }

  static from(db: Kysely<DB>) {
    return new TestContext(db).getFactory();
  }

  getFactory() {
    return TestFactory.create(this);
  }

  createUser(user: User = {}) {
    return this.userRepository.create(TestFactory.user(user));
  }

  createAsset(asset: Asset) {
    return this.assetRepository.create(TestFactory.asset(asset));
  }

  createSession(session: Session) {
    return this.sessionRepository.create(TestFactory.session(session));
  }
}