/**
 * SDK Test Runner — Domain: git-board (v0.38.5)
 *
 * Tests the real-world library composition pattern:
 * gitea-client library (connection types, exports) → git-board consumer
 * (dependency, tools, delegation). Validates the full package lifecycle
 * with realistic manifests and Starlark scripts.
 * Requires admin role.
 */
(function () {
  'use strict';
  var T = window.SDKR;
  if (!T) return;

  // ── Minimal zip builder ────────────────────────
  function buildZip(entries) {
    var localHeaders = [];
    var centralHeaders = [];
    var offset = 0;
    entries.forEach(function (entry) {
      var nameBytes = new TextEncoder().encode(entry.name);
      var dataBytes = new TextEncoder().encode(entry.content);
      var local = new Uint8Array(30 + nameBytes.length + dataBytes.length);
      var dv = new DataView(local.buffer);
      dv.setUint32(0, 0x04034b50, true);
      dv.setUint16(4, 20, true);
      dv.setUint16(8, 0, true);
      dv.setUint16(10, 0, true);
      dv.setUint16(12, 0, true);
      dv.setUint32(14, crc32(dataBytes), true);
      dv.setUint32(18, dataBytes.length, true);
      dv.setUint32(22, dataBytes.length, true);
      dv.setUint16(26, nameBytes.length, true);
      dv.setUint16(28, 0, true);
      local.set(nameBytes, 30);
      local.set(dataBytes, 30 + nameBytes.length);
      localHeaders.push(local);
      var central = new Uint8Array(46 + nameBytes.length);
      var cdv = new DataView(central.buffer);
      cdv.setUint32(0, 0x02014b50, true);
      cdv.setUint16(4, 20, true);
      cdv.setUint16(6, 20, true);
      cdv.setUint16(28, nameBytes.length, true);
      cdv.setUint32(16, crc32(dataBytes), true);
      cdv.setUint32(20, dataBytes.length, true);
      cdv.setUint32(24, dataBytes.length, true);
      cdv.setUint32(42, offset, true);
      central.set(nameBytes, 46);
      centralHeaders.push(central);
      offset += local.length;
    });
    var centralSize = centralHeaders.reduce(function (s, c) { return s + c.length; }, 0);
    var eocd = new Uint8Array(22);
    var edv = new DataView(eocd.buffer);
    edv.setUint32(0, 0x06054b50, true);
    edv.setUint16(8, entries.length, true);
    edv.setUint16(10, entries.length, true);
    edv.setUint32(12, centralSize, true);
    edv.setUint32(16, offset, true);
    var result = new Uint8Array(offset + centralSize + 22);
    var pos = 0;
    localHeaders.forEach(function (h) { result.set(h, pos); pos += h.length; });
    centralHeaders.forEach(function (h) { result.set(h, pos); pos += h.length; });
    result.set(eocd, pos);
    return new Blob([result], { type: 'application/zip' });
  }

  var crcTable = null;
  function crc32(bytes) {
    if (!crcTable) {
      crcTable = new Uint32Array(256);
      for (var n = 0; n < 256; n++) {
        var c = n;
        for (var k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
        crcTable[n] = c;
      }
    }
    var crc = 0xFFFFFFFF;
    for (var i = 0; i < bytes.length; i++) crc = crcTable[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8);
    return (crc ^ 0xFFFFFFFF) >>> 0;
  }

  function makePkgFile(manifest, extraFiles) {
    var entries = [{ name: 'manifest.json', content: JSON.stringify(manifest) }];
    if (extraFiles) entries = entries.concat(extraFiles);
    var blob = buildZip(entries);
    return new File([blob], manifest.id + '.pkg', { type: 'application/zip' });
  }

  // ── Starlark script content ────────────────────
  // Realistic but self-contained — no real HTTP calls needed.

  var LIB_HTTP_HELPERS = [
    'def auth_headers(conn):',
    '    token = conn.get("api_token", "")',
    '    if not token:',
    '        return {}',
    '    return {"Authorization": "token " + token}',
    '',
    'def _base(conn):',
    '    url = conn.get("base_url", "")',
    '    if url.endswith("/"):',
    '        url = url[:-1]',
    '    return url',
    '',
    'def gitea_get(conn, path):',
    '    url = _base(conn) + "/api/v1" + path',
    '    resp = http.get(url=url, headers=auth_headers(conn))',
    '    if int(resp["status"]) >= 400:',
    '        return None',
    '    body = resp.get("body", "")',
    '    if not body:',
    '        return None',
    '    return json.decode(body)',
    '',
    'def gitea_post(conn, path, data):',
    '    url = _base(conn) + "/api/v1" + path',
    '    hdrs = dict(auth_headers(conn), **{"Content-Type": "application/json"})',
    '    resp = http.post(url=url, body=json.encode(data), headers=hdrs)',
    '    if int(resp["status"]) >= 400:',
    '        return None',
    '    body = resp.get("body", "")',
    '    if not body:',
    '        return None',
    '    return json.decode(body)',
    '',
    'def gitea_patch(conn, path, data):',
    '    url = _base(conn) + "/api/v1" + path',
    '    hdrs = dict(auth_headers(conn), **{"Content-Type": "application/json", "X-HTTP-Method-Override": "PATCH"})',
    '    resp = http.post(url=url, body=json.encode(data), headers=hdrs)',
    '    if int(resp["status"]) >= 400:',
    '        return None',
    '    body = resp.get("body", "")',
    '    if not body:',
    '        return None',
    '    return json.decode(body)',
    '',
    'def resp(status, data):',
    '    return {"status": status, "body": json.encode(data), "headers": {"Content-Type": "application/json"}}',
  ].join('\n');

  var LIB_REPOS = [
    'load("star/http_helpers.star", "gitea_get")',
    '',
    'def get_repos(conn):',
    '    data = gitea_get(conn, "/repos/search?limit=50&sort=updated")',
    '    if data == None:',
    '        return None',
    '    repos = data if type(data) == "list" else data.get("data", [])',
    '    out = []',
    '    for r in repos:',
    '        out.append({',
    '            "full_name": r.get("full_name", ""),',
    '            "name": r.get("name", ""),',
    '            "owner": r.get("owner", {}).get("login", ""),',
    '        })',
    '    return out',
  ].join('\n');

  var LIB_ISSUES = [
    'load("star/http_helpers.star", "gitea_get", "gitea_post", "gitea_patch")',
    '',
    'def get_issues(conn, owner, repo, state):',
    '    return []',
    '',
    'def get_issue(conn, owner, repo, number):',
    '    return {"number": number, "title": "stub", "body": "", "state": "open", "labels": [], "assignee": "", "created_at": "", "comments": []}',
    '',
    'def create_issue(conn, owner, repo, title, body, labels):',
    '    return {"number": 0, "title": title, "html_url": ""}',
    '',
    'def update_issue(conn, owner, repo, number, title, body, state):',
    '    return {"number": number, "state": state or "open", "title": title or "stub"}',
    '',
    'def add_comment(conn, owner, repo, number, body):',
    '    return {"id": 0, "body": body}',
  ].join('\n');

  var LIB_CI = [
    'load("star/http_helpers.star", "gitea_get")',
    '',
    'def get_ci_status(conn, owner, repo, ref):',
    '    return {"ref": ref, "state": "pending", "statuses": [], "count": 0}',
  ].join('\n');

  var LIB_SCRIPT = [
    'load("star/http_helpers.star", "gitea_get", "gitea_post", "gitea_patch", "auth_headers", "resp")',
    'load("star/repos.star", _repos_get = "get_repos")',
    'load("star/issues.star", _issues_get = "get_issues", _issue_get = "get_issue", _issue_create = "create_issue", _issue_update = "update_issue", _comment_add = "add_comment")',
    'load("star/ci.star", _ci_get = "get_ci_status")',
    '',
    '# Re-export for lib.require() visibility',
    'get_repos = _repos_get',
    'get_issues = _issues_get',
    'get_issue = _issue_get',
    'create_issue = _issue_create',
    'update_issue = _issue_update',
    'add_comment = _comment_add',
    'get_ci_status = _ci_get',
    '',
    'def get_prs(conn, owner, repo, state):',
    '    return []',
    '',
    'def on_request(req):',
    '    conn = connections.get("gitea")',
    '    if conn == None:',
    '        return resp(400, {"error": "no connection"})',
    '    return resp(200, {"ok": True})',
  ].join('\n');

  var CONSUMER_SCRIPT = [
    'gitea = lib.require("{LIB_ID}")',
    '',
    'def _conn():',
    '    return connections.get("gitea")',
    '',
    'def on_request(req):',
    '    conn = _conn()',
    '    if conn == None:',
    '        return {"status": 400, "body": json.encode({"error": "no connection"}), "headers": {"Content-Type": "application/json"}}',
    '    return {"status": 200, "body": json.encode({"ok": True}), "headers": {"Content-Type": "application/json"}}',
    '',
    'def on_tool_call(tool_name, params):',
    '    conn = _conn()',
    '    if conn == None:',
    '        return {"error": "no connection"}',
    '    if tool_name == "list_issues":',
    '        data = gitea.get_issues(conn, params.get("owner",""), params.get("repo",""), "open")',
    '        return {"issues": data or [], "count": len(data or [])}',
    '    return {"error": "unknown tool"}',
  ].join('\n');

  // ── Tests ──────────────────────────────────────

  T.registerDomain('git-board', async function () {

    if (!window.sw || !sw.isAdmin) {
      await T.test('git-board', 'guard', 'skip — not admin', async function () {
        T.assert(true, 'skipped (user is not admin)');
      });
      return;
    }

    var ts = Date.now();
    var libId = 'gb-test-lib-' + ts;
    var consumerId = 'gb-test-board-' + ts;
    var connType = 'gb-test-gitea-' + ts;
    var connId = null;

    // ── 1. Install library with multi-file structure + connection type ──

    await T.test('git-board', 'install-library', 'install gitea-client-style library with multi-file and connection type', async function () {
      var manifest = {
        id: libId,
        title: 'GB Test Gitea Client',
        type: 'library',
        tier: 'starlark',
        version: '1.0.1',
        exports: ['get_repos', 'get_issues', 'get_issue', 'create_issue', 'update_issue', 'add_comment', 'get_prs', 'get_ci_status'],
        permissions: ['api.http', 'connections.read', 'db.read', 'db.write'],
        connections: [{
          type: connType,
          label: 'Test Gitea Instance',
          fields: {
            base_url:  { type: 'url',    required: 'true',  label: 'Server URL' },
            api_token: { type: 'secret', required: 'true',  label: 'API Token' },
            org:       { type: 'string', required: 'false', label: 'Default Org' }
          },
          scopes: ['global', 'team', 'personal']
        }],
        api_routes: [
          { method: 'GET', path: '/repos' },
          { method: 'GET', path: '/issues' }
        ],
        db_tables: {
          repos: {
            columns: { full_name: 'text', owner: 'text', name: 'text' },
            indexes: []
          }
        },
        schema_version: 1
      };
      var file = makePkgFile(manifest, [
        { name: 'script.star', content: LIB_SCRIPT },
        { name: 'star/http_helpers.star', content: LIB_HTTP_HELPERS },
        { name: 'star/repos.star', content: LIB_REPOS },
        { name: 'star/issues.star', content: LIB_ISSUES },
        { name: 'star/ci.star', content: LIB_CI }
      ]);
      var r = await sw.api.admin.packages.install(file);
      T.assert(r && r.id === libId, 'library should install');
      T.assert(r.type === 'library', 'type should be library');
    });

    T.cleanup.push(async function () {
      if (libId) try { await sw.api.admin.packages.del(libId); } catch (_) {}
    });

    // ── 2. Grant library permissions ──

    await T.test('git-board', 'grant-lib-perms', 'grant library permissions', async function () {
      await sw.api.post('/api/v1/admin/extensions/' + libId + '/permissions/grant-all', {});
      T.assert(true, 'permissions granted');
    });

    // ── 3. Connection type appears ──

    await T.test('git-board', 'conn-type-registered', 'connection type registered from library manifest', async function () {
      var types = await sw.api.connectionTypes.list();
      T.assert(Array.isArray(types), 'should be array');
      var found = types.find(function (t) { return t.type === connType; });
      T.assert(found, 'connection type should appear: ' + connType);
      T.assert(found.package_id === libId, 'package_id should be library');
      T.assert(found.fields && found.fields.api_token, 'should have api_token field');
      T.assert(found.fields.api_token.type === 'secret', 'api_token should be secret type');
      T.assert(found.fields.org, 'should have org field');
    });

    // ── 4. Install consumer with dependency + tools ──

    await T.test('git-board', 'install-consumer', 'install git-board-style consumer with dependency and tools', async function () {
      var manifest = {
        id: consumerId,
        title: 'GB Test Board',
        type: 'full',
        tier: 'starlark',
        route: '/s/' + consumerId,
        auth: 'authenticated',
        layout: 'single',
        version: '0.2.0',
        permissions: ['connections.read'],
        dependencies: {},
        api_routes: [
          { method: 'GET', path: '/repos' },
          { method: 'GET', path: '/board' },
          { method: 'GET', path: '/ci/*' }
        ],
        tools: [
          { name: 'list_issues',        description: 'List issues',    parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true } } },
          { name: 'get_issue',           description: 'Get issue',     parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, number: { type: 'number', required: true } } },
          { name: 'create_issue',        description: 'Create issue',  parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, title: { type: 'string', required: true } } },
          { name: 'update_issue',        description: 'Update issue',  parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, number: { type: 'number', required: true } } },
          { name: 'add_comment',         description: 'Add comment',   parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, number: { type: 'number', required: true }, body: { type: 'string', required: true } } },
          { name: 'list_pull_requests',  description: 'List PRs',      parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true } } },
          { name: 'get_ci_status',       description: 'Get CI status', parameters: { owner: { type: 'string', required: true }, repo: { type: 'string', required: true }, ref: { type: 'string', required: true } } }
        ],
        settings: {
          default_owner: { type: 'string', label: 'Default Owner', default: '' },
          default_repo:  { type: 'string', label: 'Default Repo',  default: '' }
        }
      };
      manifest.dependencies[libId] = '>=1.0.0';
      var script = CONSUMER_SCRIPT.replace('{LIB_ID}', libId);
      var file = makePkgFile(manifest, [
        { name: 'script.star', content: script }
      ]);
      var r = await sw.api.admin.packages.install(file);
      T.assert(r && r.id === consumerId, 'consumer should install');
      T.assert(r.type === 'full', 'type should be full');
    });

    T.cleanup.push(async function () {
      if (consumerId) try { await sw.api.admin.packages.del(consumerId); } catch (_) {}
    });

    // ── 5. Grant consumer permissions ──

    await T.test('git-board', 'grant-consumer-perms', 'grant consumer permissions', async function () {
      await sw.api.post('/api/v1/admin/extensions/' + consumerId + '/permissions/grant-all', {});
      T.assert(true, 'permissions granted');
    });

    // ── 6. Dependency recorded ──

    await T.test('git-board', 'dependency-recorded', 'consumer depends on library', async function () {
      var r = await sw.api.admin.packages.dependencies(consumerId);
      // SDK unwraps {data:[...]} to bare array
      var deps = Array.isArray(r) ? r : (r && r.data) || [];
      T.assert(deps.length > 0, 'should have dependencies');
      var dep = deps.find(function (d) { return d.library_id === libId; });
      T.assert(dep, 'dependency on library should exist');
      T.assert(dep.version_spec === '>=1.0.0', 'version spec should match');
    });

    // ── 7. Tools registered ──

    await T.test('git-board', 'tools-registered', 'consumer has 7 tools registered', async function () {
      var r = await sw.api.admin.packages.get(consumerId);
      T.assert(r, 'package should exist');
      var tools = r.tools || (r.manifest && r.manifest.tools) || [];
      T.assert(tools.length === 7, 'should have 7 tools, got ' + tools.length);
      var names = tools.map(function (t) { return t.name; }).sort();
      T.assert(names.indexOf('list_issues') >= 0, 'should include list_issues');
      T.assert(names.indexOf('get_ci_status') >= 0, 'should include get_ci_status');
      T.assert(names.indexOf('add_comment') >= 0, 'should include add_comment');
    });

    // ── 8. Library consumers listed ──

    await T.test('git-board', 'consumers-listed', 'library shows consumer in consumers list', async function () {
      var r = await sw.api.admin.packages.consumers(libId);
      // SDK unwraps {data:[...]} to bare array
      var consumers = Array.isArray(r) ? r : (r && r.data) || [];
      T.assert(consumers.length > 0, 'should have consumers');
      var found = consumers.find(function (c) { return c.consumer_id === consumerId; });
      T.assert(found, 'consumer should appear in library consumers list');
    });

    // ── 9. Uninstall protection ──

    await T.test('git-board', 'uninstall-protection', 'cannot uninstall library while consumer exists', async function () {
      var threw = false;
      try {
        await sw.api.admin.packages.del(libId);
      } catch (e) {
        threw = true;
        var msg = (e && e.message) || String(e);
        T.assert(msg.indexOf('409') >= 0 || msg.indexOf('active') >= 0 || msg.indexOf('depend') >= 0,
                 'should be 409 or dependency error: ' + msg);
      }
      T.assert(threw, 'delete should have thrown');
    });

    // ── 10. Create connection of library type ──

    await T.test('git-board', 'create-connection', 'create connection using library-declared type', async function () {
      var r = await sw.api.admin.connections.create({
        type: connType,
        package_id: libId,
        name: 'GB Test Connection',
        config: { base_url: 'https://test.example.com', api_token: 'test-token-gb', org: 'test-org' }
      });
      T.assert(r && r.id, 'should create connection');
      T.assert(r.type === connType, 'type should match');
      connId = r.id;
    });

    T.cleanup.push(async function () {
      if (connId) try { await sw.api.admin.connections.del(connId); } catch (_) {}
    });

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

    await T.test('git-board', 'cleanup-connection', 'delete test connection', async function () {
      if (connId) {
        await sw.api.admin.connections.del(connId);
        connId = null;
      }
      T.assert(true, 'connection deleted');
    });

    await T.test('git-board', 'cleanup-consumer', 'uninstall consumer', async function () {
      await sw.api.admin.packages.del(consumerId);
      consumerId = null;
      T.assert(true, 'consumer uninstalled');
    });

    await T.test('git-board', 'cleanup-library', 'uninstall library', async function () {
      await sw.api.admin.packages.del(libId);
      libId = null;
      T.assert(true, 'library uninstalled');
    });

    await T.test('git-board', 'types-removed', 'connection type removed after library uninstall', async function () {
      var types = await sw.api.connectionTypes.list();
      var found = types.find(function (t) { return t.type === connType; });
      T.assert(!found, 'type should not appear after library uninstall');
    });

  });
})();
