/**
 * Property-based tests for Entry_Generator
 *
 * Feature: incremental-server-action-build
 *
 * Properties tested:
 * - Property 5: 入口文件扫描正确排除特殊文件
 * - Property 6: moduleName 格式兼容
 * - Property 7: Auth 模块按 actionName 正确选择
 * - Property 14: 生成的入口文件导出正确接口
 * - Property 16: 并发入口文件生成的串行化
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fc from 'fast-check';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import {
  generateEntry,
  scanActionFiles,
  extractModuleName,
} from '../entry-generator';

// ─── Helpers ─────────────────────────────────────────────────────────

function createTempDir(): string {
  return fs.mkdtempSync(path.join(os.tmpdir(), 'entry-gen-prop-'));
}

function cleanDir(dir: string) {
  if (fs.existsSync(dir)) {
    fs.rmSync(dir, { recursive: true, force: true });
  }
}

// ─── Generators ──────────────────────────────────────────────────────

const platformArb = fc.constantFrom('frontend', 'backend', 'app');
const pageNameArb = fc.stringMatching(/^[A-Z][A-Za-z0-9]{1,15}$/);
const projectIdArb = fc.stringMatching(/^[a-z0-9]{4,12}$/);


// ─── Property 5: 入口文件扫描正确排除特殊文件 ────────────────────────
/**
 * Feature: incremental-server-action-build, Property 5: 入口文件扫描正确排除特殊文件
 *
 * **Validates: Requirements 3.2**
 *
 * For any set of files in server-action-generated/, Entry_Generator should
 * include all action .js files and exclude _common.js, PROJ_xxx.js, and .lock files.
 */
describe('Property 5: 入口文件扫描正确排除特殊文件', { timeout: 120_000 }, () => {
  // Generator for action filenames like src.{platform}.actions.{PageName}.js
  const actionFileArb = fc.tuple(platformArb, pageNameArb).map(
    ([platform, page]) => `src.${platform}.actions.${page}.js`
  );

  // Generator for PROJ_xxx.js filenames
  const projFileArb = projectIdArb.map((id) => `PROJ_${id}.js`);

  // Generator for .lock filenames
  const lockFileArb = fc.constantFrom('.entry-gen.lock', 'build.lock', 'test.lock');

  // Generator for non-js files
  const nonJsFileArb = fc.constantFrom(
    '_common_entry.ts',
    'README.md',
    'data.json',
    'notes.txt'
  );

  it('includes all action .js files and excludes _common.js, PROJ_xxx.js, .lock files', async () => {
    await fc.assert(
      fc.asyncProperty(
        fc.uniqueArray(actionFileArb, { minLength: 0, maxLength: 8, comparator: 'IsStrictlyEqual' }),
        fc.uniqueArray(projFileArb, { minLength: 0, maxLength: 3, comparator: 'IsStrictlyEqual' }),
        fc.uniqueArray(lockFileArb, { minLength: 0, maxLength: 2, comparator: 'IsStrictlyEqual' }),
        fc.uniqueArray(nonJsFileArb, { minLength: 0, maxLength: 3, comparator: 'IsStrictlyEqual' }),
        fc.boolean(), // whether to include _common.js
        async (actionFiles, projFiles, lockFiles, nonJsFiles, includeCommon) => {
          // Create a fresh temp dir for each iteration
          const tmpDir = createTempDir();
          try {
            // Create all files in the temp directory
            const allFiles = [
              ...actionFiles,
              ...projFiles,
              ...lockFiles,
              ...nonJsFiles,
            ];
            if (includeCommon) allFiles.push('_common.js');

            for (const file of allFiles) {
              fs.writeFileSync(path.join(tmpDir, file), '// placeholder');
            }

            // Scan
            const result = scanActionFiles(tmpDir);

            // All action files should be included
            for (const af of actionFiles) {
              expect(result).toContain(af);
            }

            // _common.js should be excluded
            expect(result).not.toContain('_common.js');

            // PROJ_xxx.js files should be excluded
            for (const pf of projFiles) {
              expect(result).not.toContain(pf);
            }

            // .lock files should be excluded
            for (const lf of lockFiles) {
              expect(result).not.toContain(lf);
            }

            // Non-js files should be excluded
            for (const nf of nonJsFiles) {
              expect(result).not.toContain(nf);
            }

            // Result should be sorted
            const sorted = [...result].sort();
            expect(result).toEqual(sorted);

            // Result length should equal the number of action files
            expect(result.length).toBe(actionFiles.length);
          } finally {
            cleanDir(tmpDir);
          }
        }
      ),
      { numRuns: 10 }
    );
  });
});


// ─── Property 6: moduleName 格式兼容 ─────────────────────────────────
/**
 * Feature: incremental-server-action-build, Property 6: moduleName 格式兼容
 *
 * **Validates: Requirements 3.3, 7.2, 7.3**
 *
 * For any PROJ_xxx.js generated by Entry_Generator, the actions Map keys
 * should be in format src.{platform}.actions.{PageName}.{functionName},
 * compatible with existing gen-server-registry.ts + bundled-entry.ts.
 */
describe('Property 6: moduleName 格式兼容', { timeout: 120_000 }, () => {
  it('generated entry file contains correct moduleName format in registry', async () => {
    await fc.assert(
      fc.asyncProperty(
        platformArb,
        pageNameArb,
        projectIdArb,
        async (platform, pageName, projectId) => {
          const tmpDir = createTempDir();
          try {
            const actionFileName = `src.${platform}.actions.${pageName}.js`;
            const expectedModuleName = `src.${platform}.actions.${pageName}`;

            // Create fake action file with an exported function
            fs.writeFileSync(
              path.join(tmpDir, actionFileName),
              'module.exports = { doSomething: function() {} };'
            );

            const result = await generateEntry({
              projectId,
              outDir: path.relative(os.tmpdir(), tmpDir),
              projectRoot: os.tmpdir(),
            });

            expect(result.success).toBe(true);

            const content = fs.readFileSync(result.outputFile, 'utf-8');

            // The registry should contain the moduleName as a key
            expect(content).toContain(`'${expectedModuleName}'`);

            // The registry entry should require the correct file
            expect(content).toContain(`require('./${expectedModuleName}')`);

            // The moduleName format should match: src.{platform}.actions.{PageName}
            const moduleNamePattern = /^src\.(frontend|backend|app)\.actions\.[A-Z][A-Za-z0-9]+$/;
            expect(expectedModuleName).toMatch(moduleNamePattern);

            // The generated code should register actions with format moduleName.fnName
            expect(content).toContain('actions.set(`${moduleName}.${fnName}`, fn)');
          } finally {
            cleanDir(tmpDir);
          }
        }
      ),
      { numRuns: 10 }
    );
  });
});


// ─── Property 7: Auth 模块按 actionName 正确选择 ─────────────────────
/**
 * Feature: incremental-server-action-build, Property 7: Auth 模块按 actionName 正确选择
 *
 * **Validates: Requirements 3.5**
 *
 * For any actionName, getAuthModule should select the correct auth module
 * based on platform identifier (.frontend./.app./other).
 */
describe('Property 7: Auth 模块按 actionName 正确选择', { timeout: 120_000 }, () => {
  // Reimplement the same getAuthModule logic for verification
  function expectedAuthModule(actionName: string): 'frontendAuth' | 'appAuth' | 'backendAuth' {
    if (actionName.includes('.frontend.') || actionName.startsWith('frontend.')) return 'frontendAuth';
    if (actionName.includes('.app.') || actionName.startsWith('app.')) return 'appAuth';
    return 'backendAuth';
  }

  // Generator for actionNames with different platform identifiers
  const frontendActionArb = fc.tuple(pageNameArb, fc.stringMatching(/^[a-z][A-Za-z0-9]{1,15}$/)).map(
    ([page, fn]) => `src.frontend.actions.${page}.${fn}`
  );

  const appActionArb = fc.tuple(pageNameArb, fc.stringMatching(/^[a-z][A-Za-z0-9]{1,15}$/)).map(
    ([page, fn]) => `src.app.actions.${page}.${fn}`
  );

  const backendActionArb = fc.tuple(pageNameArb, fc.stringMatching(/^[a-z][A-Za-z0-9]{1,15}$/)).map(
    ([page, fn]) => `src.backend.actions.${page}.${fn}`
  );

  // Also test the startsWith variants
  const frontendStartsWithArb = fc.tuple(pageNameArb, fc.stringMatching(/^[a-z][A-Za-z0-9]{1,15}$/)).map(
    ([page, fn]) => `frontend.actions.${page}.${fn}`
  );

  const appStartsWithArb = fc.tuple(pageNameArb, fc.stringMatching(/^[a-z][A-Za-z0-9]{1,15}$/)).map(
    ([page, fn]) => `app.actions.${page}.${fn}`
  );

  const actionNameArb = fc.oneof(
    frontendActionArb,
    appActionArb,
    backendActionArb,
    frontendStartsWithArb,
    appStartsWithArb,
  );

  it('getAuthModule in generated entry selects correct auth module based on platform', async () => {
    const tmpDir = createTempDir();
    try {
      // Generate an entry file to verify the getAuthModule logic is present
      fs.writeFileSync(
        path.join(tmpDir, 'src.backend.actions.Test.js'),
        'module.exports = { test: function() {} };'
      );

      const result = await generateEntry({
        projectId: 'authtest',
        outDir: path.relative(os.tmpdir(), tmpDir),
        projectRoot: os.tmpdir(),
      });

      expect(result.success).toBe(true);
      const content = fs.readFileSync(result.outputFile, 'utf-8');

      // Verify the getAuthModule function is present with correct logic
      expect(content).toContain('function getAuthModule(actionName)');
      expect(content).toContain("actionName.includes('.frontend.')");
      expect(content).toContain("actionName.startsWith('frontend.')");
      expect(content).toContain("actionName.includes('.app.')");
      expect(content).toContain("actionName.startsWith('app.')");
      expect(content).toContain('common.frontendAuth');
      expect(content).toContain('common.appAuth');
      expect(content).toContain('common.backendAuth');
    } finally {
      cleanDir(tmpDir);
    }

    // Now property-test the logic itself against random actionNames
    await fc.assert(
      fc.asyncProperty(actionNameArb, async (actionName) => {
        const expected = expectedAuthModule(actionName);

        // Verify our reference implementation matches the generated code logic
        if (actionName.includes('.frontend.') || actionName.startsWith('frontend.')) {
          expect(expected).toBe('frontendAuth');
        } else if (actionName.includes('.app.') || actionName.startsWith('app.')) {
          expect(expected).toBe('appAuth');
        } else {
          expect(expected).toBe('backendAuth');
        }
      }),
      { numRuns: 10 }
    );
  });
});


// ─── Property 14: 生成的入口文件导出正确接口 ─────────────────────────
/**
 * Feature: incremental-server-action-build, Property 14: 生成的入口文件导出正确接口
 *
 * **Validates: Requirements 7.3, 7.4**
 *
 * For any PROJ_xxx.js generated by Entry_Generator, loading it via require()
 * should return an object with path (string, format /rpc/PROJ_xxx) and
 * router (object with post method).
 *
 * Since the generated file uses require('express') and require('./_common')
 * which won't work in test context, we verify the generated content string
 * contains the correct module.exports = { path: ..., router } pattern.
 */
describe('Property 14: 生成的入口文件导出正确接口', { timeout: 120_000 }, () => {
  it('generated entry file exports { path: "/rpc/PROJ_xxx", router }', async () => {
    await fc.assert(
      fc.asyncProperty(
        projectIdArb,
        fc.uniqueArray(
          fc.tuple(platformArb, pageNameArb).map(
            ([p, n]) => `src.${p}.actions.${n}.js`
          ),
          { minLength: 0, maxLength: 5, comparator: 'IsStrictlyEqual' }
        ),
        async (projectId, actionFiles) => {
          const tmpDir = createTempDir();
          try {
            // Create fake action files
            for (const af of actionFiles) {
              fs.writeFileSync(
                path.join(tmpDir, af),
                'module.exports = { fn: function() {} };'
              );
            }

            const result = await generateEntry({
              projectId,
              outDir: path.relative(os.tmpdir(), tmpDir),
              projectRoot: os.tmpdir(),
            });

            expect(result.success).toBe(true);

            const content = fs.readFileSync(result.outputFile, 'utf-8');

            // Should define PROJECT_ID correctly
            expect(content).toContain(`const PROJECT_ID = 'PROJ_${projectId}'`);

            // Should export module.exports with path and router
            expect(content).toContain('module.exports = { path:');
            expect(content).toContain('router }');

            // The path should be /rpc/PROJ_{projectId}
            expect(content).toContain('/rpc/');
            expect(content).toContain('${PROJECT_ID}');

            // Should create an express.Router()
            expect(content).toContain('express.Router()');

            // Should have router.post('/')
            expect(content).toContain("router.post('/'");

            // The output file should be named PROJ_{projectId}.js
            expect(path.basename(result.outputFile)).toBe(`PROJ_${projectId}.js`);
          } finally {
            cleanDir(tmpDir);
          }
        }
      ),
      { numRuns: 10 }
    );
  });
});


// ─── Property 16: 并发入口文件生成的串行化 ───────────────────────────
/**
 * Feature: incremental-server-action-build, Property 16: 并发入口文件生成的串行化
 *
 * **Validates: Requirements 10.1, 10.2**
 *
 * For any set of concurrent entry file generation requests, the final
 * PROJ_xxx.js should be valid JavaScript and contain all compiled action files.
 */
describe('Property 16: 并发入口文件生成的串行化', { timeout: 120_000 }, () => {
  it('concurrent generateEntry calls produce valid output with all action files', async () => {
    await fc.assert(
      fc.asyncProperty(
        projectIdArb,
        fc.integer({ min: 2, max: 5 }), // number of concurrent calls
        fc.uniqueArray(
          fc.tuple(platformArb, pageNameArb).map(
            ([p, n]) => `src.${p}.actions.${n}.js`
          ),
          { minLength: 1, maxLength: 6, comparator: 'IsStrictlyEqual' }
        ),
        async (projectId, concurrency, actionFiles) => {
          const tmpDir = createTempDir();
          try {
            // Create fake action files
            for (const af of actionFiles) {
              fs.writeFileSync(
                path.join(tmpDir, af),
                `module.exports = { fn: function() {} };`
              );
            }

            const options = {
              projectId,
              outDir: path.relative(os.tmpdir(), tmpDir),
              projectRoot: os.tmpdir(),
            };

            // Launch concurrent generateEntry calls
            const promises = Array.from({ length: concurrency }, () =>
              generateEntry(options)
            );

            const results = await Promise.all(promises);

            // All calls should succeed (serialized via lock)
            for (const r of results) {
              expect(r.success).toBe(true);
            }

            // The final file should exist
            const outputFile = path.join(tmpDir, `PROJ_${projectId}.js`);
            expect(fs.existsSync(outputFile)).toBe(true);

            // Read the final content
            const content = fs.readFileSync(outputFile, 'utf-8');

            // Should be valid JavaScript (no syntax errors in the template)
            expect(content).toContain("require('express')");
            expect(content).toContain("require('./_common')");
            expect(content).toContain('module.exports');

            // Should contain require statements for ALL action files
            for (const af of actionFiles) {
              const moduleName = af.replace(/\.js$/, '');
              expect(content).toContain(`require('./${moduleName}')`);
            }
          } finally {
            cleanDir(tmpDir);
          }
        }
      ),
      { numRuns: 10 }
    );
  });
});
