/**
 * ICD Test Runner — Framework
 * DOM helpers, assertion library, ICD shapes, API wrappers, test harness.
 */
(function () {
  'use strict';
  var T = window.ICD;
  if (!T) return;

  // ─── DOM Helpers ────────────────────────────────────────────

  T.esc = function (s) {
    var el = document.createElement('span');
    el.textContent = String(s);
    return el.innerHTML;
  };

  T.$ = function (tag, attrs, children) {
    var el = document.createElement(tag);
    if (attrs) Object.keys(attrs).forEach(function (k) {
      if (k === 'className') el.className = attrs[k];
      else if (k === 'style' && typeof attrs[k] === 'object')
        Object.assign(el.style, attrs[k]);
      else if (k.indexOf('on') === 0)
        el.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
      else el.setAttribute(k, attrs[k]);
    });
    if (children) {
      if (!Array.isArray(children)) children = [children];
      children.forEach(function (c) {
        if (typeof c === 'string') el.appendChild(document.createTextNode(c));
        else if (c) el.appendChild(c);
      });
    }
    return el;
  };

  // ─── Assertion Library ──────────────────────────────────────

  function assertType(val, type, path) {
    var actual = val === null ? 'null' : Array.isArray(val) ? 'array' : typeof val;
    if (type === 'array') {
      if (!Array.isArray(val)) return path + ': expected array, got ' + actual;
    } else if (type === 'uuid') {
      if (typeof val !== 'string' || val.length < 8) return path + ': expected uuid string';
    } else if (type === 'string?') {
      if (val !== null && val !== undefined && typeof val !== 'string')
        return path + ': expected string|null, got ' + actual;
    } else if (type === 'number?') {
      if (val !== null && val !== undefined && typeof val !== 'number')
        return path + ': expected number|null, got ' + actual;
    } else if (type === 'bool') {
      if (typeof val !== 'boolean') return path + ': expected boolean, got ' + actual;
    } else if (type === 'object') {
      if (typeof val !== 'object' || val === null || Array.isArray(val))
        return path + ': expected object, got ' + actual;
    } else if (type === 'any') {
      // always passes
    } else {
      if (typeof val !== type) return path + ': expected ' + type + ', got ' + actual;
    }
    return null;
  }

  function validateShape(obj, schema, prefix) {
    var errors = [];
    prefix = prefix || '$';
    if (typeof obj !== 'object' || obj === null) {
      errors.push(prefix + ': expected object, got ' + (obj === null ? 'null' : typeof obj));
      return errors;
    }
    Object.keys(schema).forEach(function (key) {
      var spec = schema[key];
      var val = obj[key];
      var path = prefix + '.' + key;
      if (typeof spec === 'string') {
        if (spec.charAt(spec.length - 1) === '?') {
          if (val === undefined || val === null) return;
          spec = spec.slice(0, -1);
        } else if (val === undefined) {
          errors.push(path + ': missing required field');
          return;
        }
        var e = assertType(val, spec, path);
        if (e) errors.push(e);
      } else if (typeof spec === 'object' && spec._type) {
        if (spec._optional && (val === undefined || val === null)) return;
        if (val === undefined) { errors.push(path + ': missing required field'); return; }
        var e2 = assertType(val, spec._type, path);
        if (e2) errors.push(e2);
      }
    });
    return errors;
  }

  T.assert = function (condition, msg) {
    if (!condition) throw new Error(msg || 'assertion failed');
  };

  T.assertShape = function (obj, schema, label) {
    var errs = validateShape(obj, schema, label || '$');
    if (errs.length) throw new Error(errs.join('; '));
  };

  T.assertArrayOf = function (arr, schema, label) {
    T.assert(Array.isArray(arr), (label || '$') + ': expected array');
    if (arr.length > 0) T.assertShape(arr[0], schema, (label || '$') + '[0]');
  };

  T.assertHasKey = function (obj, key, label) {
    T.assert(obj && obj.hasOwnProperty(key), (label || '$') + ': missing key "' + key + '"');
  };

  T.assertEnum = function (val, allowed, path) {
    if (allowed.indexOf(val) === -1)
      throw new Error(path + ': "' + val + '" not in [' + allowed.join(', ') + ']');
  };

  T.assertStatus = function (data, expected, label) {
    T.assert(data._status === expected,
      (label || '$') + ': expected HTTP ' + expected + ', got ' + data._status +
      (data.error ? ' (' + data.error + ')' : ''));
  };

  // ─── ICD Shapes ─────────────────────────────────────────────

  var S = T.S;

  S.channel = { id: 'string', title: 'string', type: 'string', created_at: 'string', updated_at: 'string' };
  S.channelFull = {
    id: 'string', user_id: 'string', title: 'string', type: 'string',
    ai_mode: 'string?', topic: 'string?',
    description: 'string?', model: 'string?', provider_config_id: 'string?',
    system_prompt: 'string?', is_archived: 'bool', is_pinned: 'bool',
    folder: 'string?', folder_id: 'string?', project_id: 'string?', workspace_id: 'string?',
    tags: 'array', created_at: 'string', updated_at: 'string'
  };
  S.channelModel = { id: 'string', channel_id: 'string', model_id: 'string', is_default: 'bool', created_at: 'string' };
  S.message = { id: 'string', channel_id: 'string', role: 'string', content: 'string', created_at: 'string' };
  S.persona = { id: 'string', name: 'string', scope: 'string', created_at: 'string', updated_at: 'string' };
  S.note = { id: 'string', title: 'string', created_at: 'string', updated_at: 'string' };
  S.project = {
    id: 'string', name: 'string', scope: 'string', owner_id: 'string',
    is_archived: 'bool', created_at: 'string', updated_at: 'string',
    description: 'string?', color: 'string?', icon: 'string?',
    team_id: 'string?', workspace_id: 'string?', settings: 'object?',
    channel_count: 'number?', kb_count: 'number?', note_count: 'number?'
  };
  S.kb = {
    id: 'string', name: 'string', scope: 'string',
    embedding_config: 'object', document_count: 'number',
    chunk_count: 'number', total_bytes: 'number', status: 'string',
    created_at: 'string', updated_at: 'string'
  };
  S.folder = { id: 'string', name: 'string', created_at: 'string' };
  S.workspace = { id: 'string', name: 'string', owner_type: 'string', owner_id: 'string', status: 'string', created_at: 'string', updated_at: 'string' };
  S.gitCredSummary = { id: 'string', name: 'string', auth_type: 'string', created_at: 'string' };
  S.notification = { id: 'string', type: 'string', title: 'string', read: 'bool', created_at: 'string' };
  S.memory = { id: 'string', scope: 'string', owner_id: 'string', key: 'string', value: 'string', confidence: 'number', status: 'string', created_at: 'string', updated_at: 'string' };
  S.profile = { id: 'string', username: 'string', email: 'string', role: 'string', settings: 'object', created_at: 'string' };
  S.surface = { id: 'string', title: 'string', source: 'string', enabled: 'bool' };
  S.surfaceNav = { id: 'string', title: 'string', route: 'string' };
  S.surfaceAdmin = { id: 'string', title: 'string', manifest: 'object', enabled: 'bool', source: 'string', installed_at: 'string', updated_at: 'string' };
  S.providerConfig = { id: 'string', name: 'string', provider: 'string', scope: 'string' };
  S.safeConfig = { id: 'string', name: 'string', provider: 'string', is_active: 'bool', has_key: 'bool' };
  S.catalogModel = { model_id: 'string', provider: 'string' };
  S.modelEnabled = { id: 'string', model_id: 'string', display_name: 'string', model_type: 'string', source: 'string', provider_config_id: 'string', provider_name: 'string', provider_type: 'string', capabilities: 'object', scope: 'string', is_persona: 'bool', hidden: 'bool' };
  S.modelPreference = { id: 'string', user_id: 'string', model_id: 'string', provider_config_id: 'string', hidden: 'bool', sort_order: 'number', created_at: 'string', updated_at: 'string' };
  S.task = { id: 'string', name: 'string', task_type: 'string', schedule: 'string', is_active: 'bool', created_at: 'string' };
  S.taskFull = { id: 'string', owner_id: 'string', name: 'string', task_type: 'string', scope: 'string', schedule: 'string', timezone: 'string', is_active: 'bool', max_tokens: 'number', max_tool_calls: 'number', max_wall_clock: 'number', output_mode: 'string', run_count: 'number', created_at: 'string', updated_at: 'string' };
  S.taskRun = { id: 'string', task_id: 'string', status: 'string', started_at: 'string' };
  S.workflow = { id: 'string', name: 'string', slug: 'string', entry_mode: 'string', is_active: 'bool', created_at: 'string', updated_at: 'string' };
  S.workflowStage = { id: 'string', workflow_id: 'string', ordinal: 'number', name: 'string', history_mode: 'string', created_at: 'string', surface_pkg_id: 'string?' };
  S.workflowVersion = { id: 'string', workflow_id: 'string', version_number: 'number', created_at: 'string' };
  S.workflowStatus = { workflow_id: 'string', workflow_version: 'number', current_stage: 'number', status: 'string' };
  S.assignment = { id: 'string', channel_id: 'string', status: 'string', created_at: 'string' };
  S.team = { id: 'string', name: 'string', created_at: 'string?' };
  S.teamMember = { user_id: 'string', role: 'string' };
  S.group = { id: 'string', name: 'string' };
  S.extension = { id: 'string', title: 'string', type: 'string', version: 'string', tier: 'string', enabled: 'bool', is_system: 'bool', scope: 'string', source: 'string', installed_at: 'string', updated_at: 'string' };
  S.auditEntry = { id: 'string', action: 'string', created_at: 'string' };
  S.file = { id: 'string', filename: 'string', content_type: 'string', origin: 'string', created_at: 'string' };
  S.participant = { id: 'string', channel_id: 'string', participant_type: 'string', role: 'string', joined_at: 'string' };
  S.adminUser = { id: 'string', username: 'string', role: 'string', created_at: 'string' };
  S.loginResponse = { access_token: 'string', refresh_token: 'string' };

  // ─── Test Harness ───────────────────────────────────────────

  T.registerCleanup = function (fn) { T.cleanup.push(fn); };

  T.runCleanup = async function () {
    for (var i = T.cleanup.length - 1; i >= 0; i--) {
      try { await T.cleanup[i](); } catch (e) { /* best effort */ }
    }
    T.cleanup = [];
  };

  T.test = async function (tier, domain, name, fn) {
    var t0 = performance.now();
    var entry = { tier: tier, domain: domain, name: name, status: 'pass', duration: 0, detail: '' };
    try {
      await fn();
    } catch (e) {
      if (e && e._skip) {
        entry.status = 'skip';
        entry.detail = String(e.message || 'skipped');
      } else {
        entry.status = 'fail';
        entry.detail = String(e && e.message ? e.message : e);
      }
    }
    entry.duration = Math.round(performance.now() - t0);
    T.results.push(entry);
    if (typeof T.renderProgress === 'function') T.renderProgress();
    return entry;
  };

  /**
   * Skip a test with a reason. Call inside a T.test() fn body.
   * Skipped tests appear in results as status='skip' — not pass, not fail.
   * @param {string} reason — why this test was skipped
   */
  T.skip = function (reason) {
    var e = new Error(reason || 'skipped');
    e._skip = true;
    throw e;
  };

  // ─── API Wrappers ───────────────────────────────────────────
  // v0.37.14: raw fetch — old API._* globals removed in scorched earth.

  async function _fetchJSON(method, path, body) {
    var token = await T.getAuthToken();
    var opts = {
      method: method,
      headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
      credentials: 'same-origin'
    };
    if (body !== undefined && method !== 'GET') opts.body = JSON.stringify(body);
    var resp = await fetch(T.base + '/api/v1' + path, opts);
    if (resp.status === 204) return { _status: 204 };
    var text = await resp.text();
    var data;
    try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; }
    data._status = resp.status;
    if (!resp.ok) {
      var err = new Error(method + ' ' + path + ' → ' + resp.status + (data.error ? ': ' + data.error : ''));
      err.status = resp.status;
      err.data = data;
      throw err;
    }
    return data;
  }

  T.apiGet = async function (path) { return await _fetchJSON('GET', path); };
  T.apiPost = async function (path, body) { return await _fetchJSON('POST', path, body); };
  T.apiPut = async function (path, body) { return await _fetchJSON('PUT', path, body); };
  T.apiPatch = async function (path, body) { return await _fetchJSON('PATCH', path, body); };

  // ─── Token Capture ──────────────────────────────────────────

  var _capturedToken = '';

  T.captureAuthToken = async function () {
    if (_capturedToken) return _capturedToken;
    // v0.37.14: read directly from localStorage (old API._get interceptor removed)
    _capturedToken = getAuthTokenFromStorage();
    return _capturedToken;
  };

  function getAuthTokenFromStorage() {
    var env = (T.base || '').replace(/^\//, '') || 'default';
    var keys = ['sb_auth_' + env, 'sb_auth', 'sb_token'];
    for (var i = 0; i < keys.length; i++) {
      try {
        var raw = localStorage.getItem(keys[i]);
        if (!raw) continue;
        try {
          var parsed = JSON.parse(raw);
          if (parsed) {
            var tok = parsed.access_token || parsed.token || parsed.accessToken || '';
            if (tok) return tok;
          }
        } catch (e2) { /* not JSON */ }
        if (raw.length > 20 && raw.indexOf('{') !== 0) return raw;
      } catch (e) { /* skip */ }
    }
    return '';
  }

  T.getAuthToken = async function () {
    return _capturedToken || (await T.captureAuthToken()) || getAuthTokenFromStorage();
  };

  // ─── DELETE (needs raw fetch fallback) ──────────────────────

  T.apiDelete = async function (path) {
    return await _fetchJSON('DELETE', path);
  };

  T.safeDelete = async function (path, retries) {
    retries = retries || 2;
    for (var attempt = 0; attempt <= retries; attempt++) {
      try {
        return await T.apiDelete(path);
      } catch (e) {
        // On 502/503, wait and retry (proxy may be recovering)
        if (attempt < retries && e.message && (e.message.indexOf('502') !== -1 || e.message.indexOf('503') !== -1)) {
          await new Promise(function (r) { setTimeout(r, 500 * (attempt + 1)); });
          continue;
        }
        // Last resort: try without auth header (some DELETE endpoints accept this)
        try {
          var resp = await fetch(T.base + '/api/v1' + path, {
            method: 'DELETE', credentials: 'same-origin'
          });
          if (resp.ok || resp.status === 204) return {};
        } catch (e2) { /* give up */ }
        // Swallow cleanup errors on final attempt — don't let cleanup failures mask test results
        if (attempt >= retries) {
          console.warn('[ICD] cleanup failed: DELETE ' + path + ' — ' + e.message);
          return {};
        }
      }
    }
    return {};
  };

  // ── File Upload (multipart/form-data) ──────────────────────

  T.apiUpload = async function (path, file, filename) {
    var token = await T.getAuthToken();
    var fd = new FormData();
    fd.append('file', file, filename || 'test.txt');
    var resp = await fetch(T.base + '/api/v1' + path, {
      method: 'POST',
      headers: { 'Authorization': 'Bearer ' + token },
      credentials: 'same-origin',
      body: fd
    });
    var text = await resp.text();
    var data;
    try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; }
    data._status = resp.status;
    if (!resp.ok) throw new Error('Upload ' + path + ' → ' + resp.status + (data.error ? ': ' + data.error : ''));
    return data;
  };

  // ─── Auth Fetch (explicit token for test users) ─────────────

  T.authFetch = async function (token, method, path, body) {
    var opts = {
      method: method,
      headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
      credentials: 'same-origin'
    };
    if (body !== undefined && method !== 'GET') opts.body = JSON.stringify(body);
    var resp = await fetch(T.base + '/api/v1' + path, opts);
    var text = await resp.text();
    var data;
    try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; }
    data._status = resp.status;
    return data;
  };

  T.publicPost = async function (path, body) {
    var resp = await fetch(T.base + '/api/v1' + path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    });
    var text = await resp.text();
    var data;
    try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; }
    data._status = resp.status;
    return data;
  };

  // Session-scoped fetch: uses cookies only (no JWT). For visitor workflow tests.
  T.sessionGet = async function (path) {
    var resp = await fetch(T.base + '/api/v1' + path, {
      method: 'GET',
      credentials: 'include'
    });
    var text = await resp.text();
    var data;
    try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; }
    data._status = resp.status;
    return data;
  };

  T.sessionPost = async function (path, body) {
    var resp = await fetch(T.base + '/api/v1' + path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify(body)
    });
    var text = await resp.text();
    var data;
    try { data = text ? JSON.parse(text) : {}; } catch (e) { data = { _raw: text }; }
    data._status = resp.status;
    return data;
  };

})();
