import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { REGISTRY_COMMAND_NAMES, WEBSOCKET_COMMANDS } from '../../src/contracts/websocket-commands.js';
import { extractPayload } from '../../src/contracts/extract-payload.js';

const repoRoot = path.resolve(process.cwd(), '..');

function readRepoFile(...segments: string[]): string {
  return fs.readFileSync(path.join(repoRoot, ...segments), 'utf8');
}

function extractSwiftCommandTypes(source: string): string[] {
  const envelope = [...source.matchAll(/envelope\(type:\s*"([^"]+)"/g)].map((m) => m[1]);
  const typeOnly = [...source.matchAll(/typeOnly\("([^"]+)"/g)].map((m) => m[1]);
  const initFn = source.includes('static func connect(userID:') ? ['init'] : [];
  return [...new Set([...envelope, ...typeOnly, ...initFn])];
}

function extractKotlinCommandTypes(source: string): string[] {
  const envelope = [...source.matchAll(/envelope\(\s*"([^"]+)"/g)].map((m) => m[1]);
  const typeOnly = [...source.matchAll(/mapOf\("type"\s+to\s+"([^"]+)"\)/g)].map((m) => m[1]);
  const topLevel = [...source.matchAll(/"type"\s+to\s+"([^"]+)"/g)].map((m) => m[1]);
  return [...new Set([...envelope, ...typeOnly, ...topLevel])];
}

function walkSwiftFiles(root: string, allowlist: Set<string>): string[] {
  const violations: string[] = [];

  function walk(dir: string) {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        walk(full);
      } else if (entry.name.endsWith('.swift') && !allowlist.has(full)) {
        const source = fs.readFileSync(full, 'utf8');
        if (source.includes('["type":') && !source.includes('WebSocketCommands.')) {
          violations.push(path.relative(repoRoot, full));
        }
      }
    }
  }

  walk(root);
  return violations;
}

describe('client → registry reverse coverage', () => {
  const iosCommands = readRepoFile('ios', 'lichunWebsocket', 'Shared', 'Utilities', 'Constants.swift');
  const androidCommands = readRepoFile(
    'android',
    'app',
    'src',
    'main',
    'java',
    'com',
    'craigvg',
    'lichun_android',
    'utils',
    'WebSocketCommands.kt'
  );

  const iosTypes = extractSwiftCommandTypes(iosCommands);
  const androidTypes = extractKotlinCommandTypes(androidCommands);

  it('iOS WebSocketCommands only reference registered commands (or init)', () => {
    const allowed = new Set([...REGISTRY_COMMAND_NAMES, 'init']);
    const unknown = iosTypes.filter((type) => !allowed.has(type));
    expect(unknown).toEqual([]);
  });

  it('Android WebSocketCommands only reference registered commands (or init)', () => {
    const allowed = new Set([...REGISTRY_COMMAND_NAMES, 'init']);
    const unknown = androidTypes.filter((type) => !allowed.has(type));
    expect(unknown).toEqual([]);
  });

  it('manifest clients list includes platforms that expose each builder command', () => {
    for (const type of iosTypes) {
      if (type === 'init') continue;
      expect(WEBSOCKET_COMMANDS[type]?.clients).toContain('ios');
    }
    for (const type of androidTypes) {
      if (type === 'init') continue;
      expect(WEBSOCKET_COMMANDS[type]?.clients).toContain('android');
    }
  });
});

describe('client send enforcement', () => {
  it('iOS app code does not use raw WebSocket type sends outside WebSocketCommands', () => {
    const iosRoot = path.join(repoRoot, 'ios', 'lichunWebsocket');
    const allowlist = new Set([
      path.join(iosRoot, 'Shared', 'Utilities', 'Constants.swift'),
    ]);

    const violations = walkSwiftFiles(iosRoot, allowlist);
    expect(violations).toEqual([]);
  });

  it('Android WebSocketManager does not construct inline command maps', () => {
    const ws = readRepoFile(
      'android',
      'app',
      'src',
      'main',
      'java',
      'com',
      'craigvg',
      'lichun_android',
      'network',
      'WebSocketManager.kt'
    );

    expect(ws).not.toMatch(/sendMessage\(mapOf\(\s*"type"/);
    expect(ws).toContain('WebSocketCommands.');
  });
});

describe('envelope round-trip for builder commands', () => {
  it('extracts canonical iOS builder envelopes consistently', () => {
    const samples: Array<{ type: string; envelope: Record<string, unknown> }> = [
      { type: 'quitHabit', envelope: { type: 'quitHabit', message: { habitId: 'smoking' } } },
      { type: 'getAchievements', envelope: { type: 'getAchievements' } },
      { type: 'start', envelope: { type: 'command', message: 'start' } },
    ];

    for (const sample of samples) {
      expect(extractPayload(sample.envelope).commandType).toBe(sample.type);
    }
  });
});
