/**
 * 이 파일이 하는 일:
 * 학생이 작성한 JavaScript 코드를 "안전하게" 실행하고 테스트케이스로 채점한다.
 *
 * 핵심 아이디어 — Web Worker.
 * 학습 포인트: 브라우저에서 남의 코드를 그냥 eval()로 돌리면 두 가지가 위험하다.
 *   1) while(true) 같은 무한루프가 들어오면 화면이 통째로 얼어붙는다(멈출 방법이 없다).
 *   2) 그 코드가 document를 만져 화면을 부수거나 쿠키를 훔쳐볼 수 있다.
 * Web Worker는 "다른 스레드에서 도는 별도의 방"이다.
 *   1) 오래 걸리면 terminate()로 방을 통째로 없애 버릴 수 있다 → 무한루프 방어
 *   2) Worker 안에는 document도 window도 없다 → 화면을 만질 수 없다
 * 그래서 서버에 Docker 샌드박스를 짓지 않고도, 브라우저가 이미 가진 격리 장치를 빌려 쓴다.
 *
 * ⚠️ 이 안전함이 성립하는 조건 — 반드시 지킬 것.
 * Worker는 완벽한 감옥이 아니다. document는 없지만 fetch는 되고, 같은 출처 API를
 * "그 브라우저의 세션 쿠키를 달고" 호출할 수 있다.
 * 지금 안전한 이유는 단 하나 — "자기 코드를 자기 브라우저에서 자기 권한으로만" 돌리기 때문이다.
 * (실행 지점은 CodingProblemPage 한 곳뿐이고, 코드는 서버가 본인 것만 내려준다.)
 *
 * 그러므로 절대 하지 말 것:
 *   ❌ 멘토 화면에 "학생 코드 실행해 보기" 버튼 붙이기
 *      → 학생이 fetch('/api/mentor/...')를 심어두면 멘토 세션으로 실행된다 = 권한 탈취.
 * 남의 코드를 남의 브라우저에서 돌려야 한다면, 그때는 서버 샌드박스가 필요하다.
 */

/** 코드 한 번 실행에 허용할 시간(ms). 넘으면 무한루프로 보고 Worker를 없앤다. */
const TIMEOUT_MS = 2000;

/**
 * Worker 안에서 돌 코드.
 * 학습 포인트: Worker는 별도 파일이 필요한데, 파일을 따로 두는 대신
 * 문자열을 Blob으로 만들어 URL처럼 쓴다(createObjectURL).
 * "파일 하나 늘리지 않고 Worker 쓰기" 관용구다.
 */
const workerSource = `
self.onmessage = function (event) {
  var code = event.data.code;
  var functionName = event.data.functionName;
  var tests = event.data.tests;
  var results = [];

  // 학생 코드를 평가해서 함수를 꺼낸다.
  var fn;
  try {
    // 학습 포인트: new Function(코드문자열)은 그 코드를 담은 함수를 만들어 준다.
    // 뒤에 return을 덧붙여, 학생이 정의한 함수를 밖으로 꺼내오게 했다.
    // eval()과 달리 바깥 지역 변수에 접근하지 못해(전역 스코프에서 평가된다)
    // 우리 채점 코드의 변수를 학생 코드가 건드릴 수 없다.
    var factory = new Function(code + '\\nreturn typeof ' + functionName + ' === "function" ? ' + functionName + ' : null;');
    fn = factory();
  } catch (e) {
    self.postMessage({ error: '코드에 문법 오류가 있습니다: ' + e.message });
    return;
  }
  if (!fn) {
    self.postMessage({ error: '"' + functionName + '" 함수를 찾을 수 없습니다. 함수 이름을 바꾸지 마세요.' });
    return;
  }

  for (var i = 0; i < tests.length; i++) {
    var test = tests[i];
    var actual;
    var errorMessage = null;
    try {
      // 인자를 복사해서 넘긴다 — 학생 코드가 배열을 바꿔도 다음 테스트에 영향이 없도록.
      var args = JSON.parse(JSON.stringify(test.args));
      actual = fn.apply(null, args);
    } catch (e) {
      errorMessage = e.message;
    }

    var expectedText = JSON.stringify(test.expected);
    var actualText = errorMessage ? null : JSON.stringify(actual);
    // 학습 포인트: 배열/객체는 ===로 비교하면 "내용이 같아도" false다(주소를 비교하니까).
    // JSON 문자열로 바꿔 비교하면 내용 비교가 된다. 키 순서까지 같아야 하는 한계는 있지만
    // 우리 문제 범위에서는 충분하다.
    var passed = !errorMessage && actualText === expectedText;

    results.push({
      name: test.name,
      passed: passed,
      expected: expectedText,
      actual: errorMessage ? ('에러: ' + errorMessage) : (actualText === undefined ? 'undefined' : actualText)
    });
  }

  self.postMessage({ results: results });
};
`;

/**
 * 학생 코드를 테스트케이스로 채점한다.
 *
 * @param {string} code 학생이 작성한 코드
 * @param {string} functionName 호출할 함수 이름
 * @param {Array} tests [{name, args, expected}]
 * @returns {Promise<{results?: Array, error?: string}>}
 */
export function runJs(code, functionName, tests) {
  return new Promise((resolve) => {
    const blob = new Blob([workerSource], { type: 'application/javascript' });
    const url = URL.createObjectURL(blob);
    const worker = new Worker(url);

    // 끝났든 시간이 지났든, 뒷정리는 한 곳에서 한다.
    let settled = false;
    function finish(value) {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      worker.terminate();
      URL.revokeObjectURL(url);
      resolve(value);
    }

    const timer = setTimeout(() => {
      finish({ error: `실행이 ${TIMEOUT_MS / 1000}초를 넘겼습니다. 무한루프가 아닌지 확인해 보세요.` });
    }, TIMEOUT_MS);

    worker.onmessage = (event) => finish(event.data);
    worker.onerror = (event) => finish({ error: '실행 중 오류: ' + event.message });

    worker.postMessage({ code, functionName, tests });
  });
}
