import { readFileSync, readdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
import * as ts from 'typescript';

const sourceRoot = resolve(__dirname);
const validObjectId = '507f1f77bcf86cd799439011';

type Constructor = new (...args: any[]) => any;
type Operation = { dependency: number; method: string; args: unknown[] };

function collectAdapterFiles(directory: string): string[] {
  return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
    const path = join(directory, entry.name);
    if (entry.isDirectory()) {
      return collectAdapterFiles(path);
    }
    return entry.name.endsWith('.controller.ts') || entry.name.endsWith('.repository.ts') ? [path] : [];
  });
}

function publicMethodParameters(file: string, className: string): Map<string, string[]> {
  const source = ts.createSourceFile(
    file,
    readFileSync(file, 'utf8'),
    ts.ScriptTarget.Latest,
    true,
    ts.ScriptKind.TS,
  );
  const methods = new Map<string, string[]>();

  source.forEachChild((node) => {
    if (!ts.isClassDeclaration(node) || node.name?.text !== className) return;
    for (const member of node.members) {
      if (!ts.isMethodDeclaration(member) || !member.name) continue;
      const isPrivate = member.modifiers?.some(
        (modifier) =>
          modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.ProtectedKeyword,
      );
      if (isPrivate) continue;
      methods.set(
        member.name.getText(source).replace(/^['"]|['"]$/g, ''),
        member.parameters.map((parameter) => parameter.name.getText(source)),
      );
    }
  });

  return methods;
}

function createTransportValue(): any {
  const target = function transportValue() {
    return value;
  };
  Object.assign(target, {
    status: 'ready',
    success: true,
    checks: {},
    items: [],
    data: [],
    user: {
      sub: validObjectId,
      userId: validObjectId,
      id: validObjectId,
      email: 'member@example.com',
      permissions: [],
    },
  });
  const value: any = new Proxy(target, {
    apply: () => value,
    get: (object, property) => {
      if (property === 'then') return undefined;
      if (property === Symbol.iterator) return function* emptyIterator() {};
      if (property === Symbol.toPrimitive) return () => validObjectId;
      if (property === 'toString' || property === 'valueOf') return () => validObjectId;
      if (property === 'map' || property === 'filter' || property === 'slice') return () => [];
      if (Reflect.has(object, property)) return Reflect.get(object, property);
      return value;
    },
  });
  return value;
}

function representativePayload() {
  return {
    id: validObjectId,
    _id: validObjectId,
    sub: validObjectId,
    userId: validObjectId,
    authorId: validObjectId,
    ownerId: validObjectId,
    ownerAdminId: validObjectId,
    postId: validObjectId,
    commentId: validObjectId,
    targetId: validObjectId,
    conversationId: validObjectId,
    participantId: validObjectId,
    recipientId: validObjectId,
    parentCommentId: validObjectId,
    email: 'member@example.com',
    username: 'member',
    name: 'Member',
    content: 'contract probe',
    title: 'Contract probe',
    reason: 'contract probe',
    type: 'post',
    targetType: 'post',
    resourceType: 'post',
    status: 'active',
    action: 'activate',
    isActive: true,
    isDisabled: false,
    targetIds: [validObjectId],
    participantIds: [validObjectId],
    userIds: [validObjectId],
    deviceId: 'device-contract',
    fcmToken: 'token-contract',
    imageUrls: [],
    tags: [],
    page: 1,
    limit: 2,
  };
}

function argumentFor(parameterName: string): unknown {
  const name = parameterName.toLowerCase();
  const upload = {
    originalname: 'image.jpg',
    mimetype: 'image/jpeg',
    size: 3,
    buffer: Buffer.from([0xff, 0xd8, 0xff]),
  };
  if (name === 'req' || name === 'request' || name.endsWith('request')) {
    return {
      user: representativePayload(),
      headers: { 'x-request-id': 'contract-request' },
      method: 'GET',
      originalUrl: '/contract',
    };
  }
  if (name === 'res' || name.includes('response')) {
    const response: any = new Proxy(
      {},
      {
        get: (target, property) => {
          if (!Reflect.has(target, property)) {
            Reflect.set(target, property, jest.fn(() => response));
          }
          return Reflect.get(target, property);
        },
      },
    );
    return response;
  }
  if (name === 'user' || name.includes('currentuser')) return representativePayload();
  if (name.includes('files')) {
    return Object.assign([upload], {
      imageFiles: [upload],
      shopImageFiles: [upload],
      avatarFile: [upload],
      audioFile: [upload],
      videoFile: [upload],
    });
  }
  if (name.includes('file')) return upload;
  if (/(^|_)(ids|idlist)$/.test(name) || name.endsWith('ids')) return [validObjectId];
  if (name.endsWith('id') || name === 'jti') return validObjectId;
  if (name.includes('limit')) return 2;
  if (name.includes('skip') || name.includes('offset')) return 0;
  if (name.includes('page')) return 1;
  if (name.includes('direction')) return -1;
  if (name.includes('session')) return undefined;
  if (name.includes('email')) return 'member@example.com';
  if (name.endsWith('names') || name === 'usernames') return ['member'];
  if (name.includes('text') || name.includes('content') || name.includes('title') || name.includes('reason')) {
    return 'contract probe';
  }
  if (name === 'q' || name.includes('search')) return 'contract';
  if (name.includes('sort')) return { createdAt: -1 };
  if (name.includes('filter')) return {};
  return representativePayload();
}

function createControllerDependency(index: number, operations: Operation[]) {
  const methods = new Map<PropertyKey, jest.Mock>();
  return new Proxy(
    {},
    {
      get: (_target, property) => {
        if (property === 'then') return undefined;
        if (!methods.has(property)) {
          methods.set(
            property,
            jest.fn((...args: unknown[]) => {
              operations.push({ dependency: index, method: String(property), args });
              return createTransportValue();
            }),
          );
        }
        return methods.get(property);
      },
    },
  );
}

function createRepositoryModel(index: number, operations: Operation[]) {
  const document: any = {
    id: validObjectId,
    _id: validObjectId,
    participantIds: [validObjectId],
    read: false,
    modifiedCount: 1,
    deletedCount: 1,
    populate: jest.fn(async () => document),
    save: jest.fn(async (...args: unknown[]): Promise<unknown> => {
      operations.push({ dependency: index, method: 'document.save', args });
      return document;
    }),
  };
  const createQuery = () => {
    const queryResult: any[] = [];
    Object.assign(queryResult, document);
    const query: any = new Proxy(
      {},
      {
        get: (_target, property) => {
          if (property === 'then') return undefined;
          if (property === 'exec') {
            return jest.fn(async () => {
              operations.push({ dependency: index, method: 'query.exec', args: [] });
              return queryResult;
            });
          }
          return jest.fn((...args: unknown[]) => {
            operations.push({ dependency: index, method: `query.${String(property)}`, args });
            return query;
          });
        },
      },
    );
    return query;
  };
  const methods = new Map<PropertyKey, jest.Mock>();
  const modelTarget = function modelConstructor() {
    return document;
  };

  return new Proxy(modelTarget, {
    construct: (_target, args) => {
      operations.push({ dependency: index, method: 'new', args });
      return document;
    },
    get: (_target, property) => {
      if (property === 'collection') return { name: `collection_${index}` };
      if (!methods.has(property)) {
        methods.set(
          property,
          jest.fn((...args: unknown[]) => {
            operations.push({ dependency: index, method: String(property), args });
            return property === 'create'
              ? Promise.resolve(Array.isArray(args[0]) ? [document] : document)
              : createQuery();
          }),
        );
      }
      return methods.get(property);
    },
  });
}

describe('controller and repository contracts', () => {
  const files = collectAdapterFiles(sourceRoot);

  it.each(files.filter((file) => file.endsWith('.controller.ts')))(
    '%s delegates every public handler or enforces its documented local rejection',
    async (file) => {
      const moduleExports = require(file) as Record<string, Constructor>;
      const controllers = Object.values(moduleExports).filter(
        (exported): exported is Constructor =>
          typeof exported === 'function' && exported.name.endsWith('Controller'),
      );
      expect(controllers).not.toHaveLength(0);

      for (const Controller of controllers) {
        const operations: Operation[] = [];
        const dependencies = Array.from({ length: Controller.length }, (_, index) =>
          createControllerDependency(index, operations),
        );
        const instance = Reflect.construct(Controller, dependencies);
        const methods = publicMethodParameters(file, Controller.name);
        expect(methods.size).toBeGreaterThan(0);

        for (const [method, parameters] of methods) {
          const key = `${Controller.name}.${method}`;
          const args = parameters.map(argumentFor);
          const before = operations.length;

          if (key === 'ChatController.messagesMissingConversationId') {
            await expect(Promise.resolve().then(() => instance[method](...args))).rejects.toThrow(
              'conversationId is required',
            );
            expect(operations).toHaveLength(before);
            continue;
          }
          if (key === 'AuthController.googleAuth') {
            await expect(Promise.resolve(instance[method](...args))).resolves.toBeUndefined();
            expect(operations).toHaveLength(before);
            continue;
          }

          try {
            await instance[method](...args);
          } catch (error) {
            const detail = error instanceof Error ? error.message : String(error);
            throw new Error(`${key} failed its delegation contract: ${detail}`);
          }
          const delegated = operations.slice(before);
          if (!delegated.length) {
            throw new Error(`${key} did not delegate to an injected dependency`);
          }
          expect(delegated[0]).toEqual(
            expect.objectContaining({ dependency: expect.any(Number), method: expect.any(String) }),
          );
        }
      }
    },
  );

  it.each(files.filter((file) => file.endsWith('.repository.ts')))(
    '%s routes every public operation through its persistence model',
    async (file) => {
      const moduleExports = require(file) as Record<string, Constructor>;
      const repositories = Object.values(moduleExports).filter(
        (exported): exported is Constructor =>
          typeof exported === 'function' && exported.name.endsWith('Repository'),
      );
      expect(repositories).not.toHaveLength(0);

      for (const Repository of repositories) {
        const operations: Operation[] = [];
        const dependencies = Array.from({ length: Repository.length }, (_, index) =>
          createRepositoryModel(index, operations),
        );
        const instance = Reflect.construct(Repository, dependencies);
        const methods = publicMethodParameters(file, Repository.name);
        expect(methods.size).toBeGreaterThan(0);

        for (const [method, parameters] of methods) {
          const before = operations.length;
          const args = parameters.map(argumentFor);
          if (`${Repository.name}.${method}` === 'FollowsRepository.isDuplicateKeyError') {
            expect(instance[method]({ code: 11000 })).toBe(true);
            expect(instance[method]({ code: 1 })).toBe(false);
            expect(operations).toHaveLength(before);
            continue;
          }
          if (`${Repository.name}.${method}` === 'FollowsRepository.runInTransaction') {
            // Session lifecycle and fallback semantics need a session-aware model mock and
            // are covered in follows.repository.spec.ts.
            continue;
          }
          try {
            await instance[method](...args);
          } catch (error) {
            const detail = error instanceof Error ? error.message : String(error);
            throw new Error(`${Repository.name}.${method} failed its persistence contract: ${detail}`);
          }

          const persistenceCalls = operations.slice(before);
          if (!persistenceCalls.length) {
            throw new Error(`${Repository.name}.${method} did not reach an injected persistence model`);
          }
          expect(persistenceCalls.some((operation) => operation.method !== 'toString')).toBe(true);
        }
      }
    },
  );
});
