/**
 * SDK Test Runner — Framework
 *
 * Dual-path validation: every test runs through the SDK first. If it
 * fails, a raw fetch against the ICD-specified endpoint isolates the
 * fault:
 *
 *   SDK pass → PASS (both layers good)
 *   SDK fail + ICD pass → SDK_BUG (wrong method/path/unwrap)
 *   SDK fail + ICD fail → ICD_BUG (backend broken)
 *   Validate fail → SHAPE_BUG (response doesn't match ICD contract)
 */
(function () {
  'use strict';
  var T = window.SDKR;
  if (!T) return;

  // ─── Results ───────────────────────────────────────────────

  T.results   = [];
  T.cleanup   = [];
  T.stats     = { pass: 0, sdk_bug: 0, icd_bug: 0, shape_bug: 0, skip: 0 };
  T.running   = false;
  T.aborted   = false;

  // ─── 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'; }
    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 bool, got ' + actual; }
    else if (type === 'object')  { if (typeof val !== 'object' || val === null || Array.isArray(val)) return path + ': expected object, got ' + actual; }
    else if (type === 'object?') { if (val !== null && val !== undefined && (typeof val !== 'object' || Array.isArray(val))) return path + ': expected object|null, 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) {
      return [prefix + ': expected object, got ' + (obj === null ? 'null' : typeof obj)];
    }
    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);
      }
    });
    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, got ' + (typeof arr));
    if (arr.length > 0) T.assertShape(arr[0], schema, (label || '$') + '[0]');
  };

  /**
   * Unwrap SDK list response — handles both bare array and paginated envelope.
   * SDK _unwrap returns { data: [...], total, ... } when pagination metadata
   * exists, or a bare array when the envelope has only { data: [...] }.
   */
  T.unwrapList = function (r) {
    if (Array.isArray(r)) return r;
    if (r && Array.isArray(r.data)) return r.data;
    throw new Error('expected array or { data: [...] } envelope, got ' + (typeof r));
  };

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

  // ─── Raw Fetch (ICD path) ─────────────────────────────────

  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' && method !== 'DELETE')
      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.raw = {
    get:    function (path)       { return _fetchJSON('GET', path); },
    post:   function (path, body) { return _fetchJSON('POST', path, body); },
    put:    function (path, body) { return _fetchJSON('PUT', path, body); },
    patch:  function (path, body) { return _fetchJSON('PATCH', path, body); },
    del:    function (path)       { return _fetchJSON('DELETE', path); },
  };

  T.safeDelete = async function (path) {
    try { await T.raw.del(path); } catch (e) {
      console.warn('[SDKR] cleanup failed: DELETE ' + path + ' — ' + e.message);
    }
  };

  // ─── Auth Token ────────────────────────────────────────────

  var _token = '';

  function _readToken() {
    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 p = JSON.parse(raw);
          if (p) { var t = p.access_token || p.token || p.accessToken || ''; if (t) return t; }
        } catch (_) { /* not JSON */ }
        if (raw.length > 20 && raw.indexOf('{') !== 0) return raw;
      } catch (_) { /* skip */ }
    }
    return '';
  }

  T.getAuthToken = async function () {
    if (!_token) _token = _readToken();
    return _token;
  };

  // ─── Cleanup ───────────────────────────────────────────────

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

  T.runCleanup = async function () {
    var fns = T.cleanup.splice(0);
    for (var i = fns.length - 1; i >= 0; i--) {
      try { await fns[i](); } catch (_) { /* swallow */ }
    }
  };

  // ─── Dual-Path Test ────────────────────────────────────────
  //
  //  sdkFn:    async () => result   — calls sw.api.*
  //  rawSpec:  { method, path, body? }  — ICD-correct HTTP call
  //  validate: (result) => void     — throws if shape is wrong
  //
  //  Returns: { verdict, sdkErr?, rawErr?, result? }

  T.dualTest = async function (sdkFn, rawSpec, validate) {
    // 1. Try SDK
    var sdkResult, sdkErr;
    try {
      sdkResult = await sdkFn();
    } catch (e) {
      sdkErr = e;
    }

    if (!sdkErr) {
      // SDK call succeeded — validate shape
      try {
        if (validate) validate(sdkResult);
        return { verdict: 'PASS', result: sdkResult };
      } catch (e) {
        // Shape mismatch — might be _unwrap bug. Confirm with raw.
        var rawResult2;
        try {
          rawResult2 = await _fetchJSON(rawSpec.method, rawSpec.path, rawSpec.body);
        } catch (_) { /* ignore raw errors in this branch */ }
        return { verdict: 'SHAPE_BUG', sdkErr: e.message, sdkResult: sdkResult, rawResult: rawResult2 };
      }
    }

    // 2. SDK failed — try raw ICD fetch
    if (!rawSpec) {
      return { verdict: 'SDK_BUG', sdkErr: sdkErr.message, note: 'no raw spec provided' };
    }

    var rawResult, rawErr;
    try {
      rawResult = await _fetchJSON(rawSpec.method, rawSpec.path, rawSpec.body);
    } catch (e) {
      rawErr = e;
    }

    if (rawErr) {
      // Both failed → ICD/backend issue
      return { verdict: 'ICD_BUG', sdkErr: sdkErr.message, rawErr: rawErr.message };
    }

    // Raw succeeded, SDK failed → SDK has a bug
    // Optionally validate the raw result too
    var rawValid = true;
    if (validate) {
      try { validate(rawResult); } catch (e) { rawValid = false; }
    }

    return {
      verdict: 'SDK_BUG',
      sdkErr: sdkErr.message,
      rawResult: rawResult,
      rawValid: rawValid
    };
  };

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

  /**
   * Register and run a single test.
   *
   * @param {string} domain — e.g. 'channels', 'workflows'
   * @param {string} group  — e.g. 'crud', 'list', 'update'
   * @param {string} name   — human-readable test name
   * @param {object} spec   — { sdk, raw, validate }
   *   sdk:      async () => result
   *   raw:      { method: 'GET'|'POST'|..., path: '/channels', body?: {...} }
   *   validate: (result) => void  (throw on failure)
   *
   * For tests that don't use dual-path (e.g. setup/cleanup), pass a
   * plain async function as the 4th argument instead of a spec object.
   */
  T.test = async function (domain, group, name, specOrFn) {
    if (T.aborted) return;

    var entry = {
      domain: domain, group: group, name: name,
      verdict: null, sdkErr: null, rawErr: null, note: null,
      durationMs: 0
    };

    var t0 = performance.now();

    try {
      // Skip support — mark as SKIP with reason
      if (typeof specOrFn === 'object' && specOrFn.skip) {
        entry.verdict = 'SKIP';
        entry.note = specOrFn.skip;
      } else if (typeof specOrFn === 'function') {
        // Simple test — no dual-path
        await specOrFn();
        entry.verdict = 'PASS';
      } else {
        // Dual-path test
        var r = await T.dualTest(specOrFn.sdk, specOrFn.raw, specOrFn.validate);
        entry.verdict  = r.verdict;
        entry.sdkErr   = r.sdkErr || null;
        entry.rawErr   = r.rawErr || null;
        entry.note     = r.note || null;
      }
    } catch (e) {
      entry.verdict = 'ERROR';
      entry.sdkErr  = e.message;
    }

    entry.durationMs = Math.round(performance.now() - t0);

    // Update stats
    var v = entry.verdict.toLowerCase().replace('error', 'icd_bug');
    if (T.stats.hasOwnProperty(v)) T.stats[v]++;

    T.results.push(entry);

    // Live update
    if (typeof T.onResult === 'function') T.onResult(entry);
  };

  // ─── Domain Registry ───────────────────────────────────────

  T.domains = {};

  T.registerDomain = function (name, fn) {
    T.domains[name] = fn;
  };

  // ─── Run All ───────────────────────────────────────────────

  T.runAll = async function (domainFilter) {
    T.running  = true;
    T.aborted  = false;
    T.results  = [];
    T.cleanup  = [];
    T.stats    = { pass: 0, sdk_bug: 0, icd_bug: 0, shape_bug: 0, skip: 0 };

    if (typeof T.onStart === 'function') T.onStart();

    // Provision fixtures (policies, provider, team) before domains
    if (typeof T.provisionFixtures === 'function') {
      try { await T.provisionFixtures(); } catch (e) {
        console.warn('[SDKR] Fixture provisioning failed:', e.message);
      }
    }

    var names = Object.keys(T.domains).sort();
    if (domainFilter) {
      names = names.filter(function (n) { return domainFilter.indexOf(n) !== -1; });
    }

    for (var i = 0; i < names.length; i++) {
      if (T.aborted) break;
      try {
        await T.domains[names[i]]();
      } catch (e) {
        T.results.push({
          domain: names[i], group: 'setup', name: 'domain crashed',
          verdict: 'ERROR', sdkErr: e.message, rawErr: null, note: null,
          durationMs: 0
        });
      }
      // Run cleanup between domains
      await T.runCleanup();
    }

    // Teardown fixtures (revert policies, etc.)
    if (typeof T.teardownFixtures === 'function') {
      try { await T.teardownFixtures(); } catch (_) {}
    }

    T.running = false;
    if (typeof T.onComplete === 'function') T.onComplete();
  };

  T.abort = function () { T.aborted = true; };

})();
