/**
 * ICD Test Runner — CRUD: Knowledge Bases
 * KB lifecycle — create, read, update, documents, search, discoverable,
 * channel binding, scope auth, cross-user isolation, delete.
 */
(function () {
  'use strict';
  var T = window.ICD;
  if (!T) return;
  if (!T.crud) T.crud = {};

  T.crud.knowledge = async function (testTag) {

    // ── KB CRUD ──
    var kbId = null;
    await T.test('crud', 'knowledge', 'POST /knowledge-bases (create)', async function () {
      var d = await T.apiPost('/knowledge-bases', {
        name: testTag + '-kb',
        description: 'ICD integration test KB',
        scope: 'personal'
      });
      T.assertShape(d, T.S.kb, 'kb');
      T.assert(d.scope === 'personal', 'scope should be personal');
      T.assert(d.status === 'active', 'status should be active');
      T.assert(d.document_count === 0, 'new KB should have 0 documents');
      T.assert(d.chunk_count === 0, 'new KB should have 0 chunks');
      kbId = d.id;
      T.registerCleanup(function () { if (kbId) return T.safeDelete('/knowledge-bases/' + kbId); });
    });

    if (!kbId) return;

    await T.test('crud', 'knowledge', 'GET /knowledge-bases/:id (read)', async function () {
      var d = await T.apiGet('/knowledge-bases/' + kbId);
      T.assertShape(d, T.S.kb, 'kb');
      T.assert(d.id === kbId, 'id mismatch');
    });

    await T.test('crud', 'knowledge', 'PUT /knowledge-bases/:id (update)', async function () {
      var newName = testTag + '-kb-updated';
      var d = await T.apiPut('/knowledge-bases/' + kbId, { name: newName });
      T.assertShape(d, T.S.kb, 'kb-update');
      T.assert(d.name === newName, 'name should have updated, got: ' + d.name);
    });

    await T.test('crud', 'knowledge', 'PUT /knowledge-bases/:id (empty body → 400)', async function () {
      var token = await T.getAuthToken();
      var resp = await T.authFetch(token, 'PUT', '/knowledge-bases/' + kbId, {});
      T.assert(resp._status === 400, 'expected 400 for empty update, got ' + resp._status);
    });

    // ── Documents ──
    await T.test('crud', 'knowledge', 'GET /knowledge-bases/:id/documents (empty)', async function () {
      var d = await T.apiGet('/knowledge-bases/' + kbId + '/documents');
      T.assertHasKey(d, 'data', '/kb-documents');
      T.assert(Array.isArray(d.data), 'data should be array');
      T.assert(d.data.length === 0, 'new KB should have 0 documents');
    });

    // Document upload → status → delete (requires object store)
    var docId = null;
    var storageOk = false;
    try {
      var ss = await T.apiGet('/admin/storage/status');
      storageOk = ss && ss.configured === true;
    } catch (e) { /* non-admin or storage not configured */ }

    if (storageOk) {
      await T.test('crud', 'knowledge', 'POST /knowledge-bases/:id/documents (upload)', async function () {
        var blob = new Blob(['# ICD Test Document\n\nThis is test content for KB ingestion.'], { type: 'text/markdown' });
        try {
          var d = await T.apiUpload('/knowledge-bases/' + kbId + '/documents', blob, 'icd-test.md');
          T.assert(d.id, 'expected document id');
          T.assert(d.filename === 'icd-test.md', 'filename mismatch: ' + d.filename);
          T.assert(d.status === 'pending', 'initial status should be pending, got: ' + d.status);
          T.assert(d.kb_id === kbId, 'kb_id mismatch');
          docId = d.id;
        } catch (e) {
          // 412 = no embedding model role configured (infrastructure, not contract bug)
          if (e.message && (e.message.indexOf('412') !== -1 || e.message.indexOf('embedding') !== -1)) return;
          throw e;
        }
      });

      if (docId) {
        await T.test('crud', 'knowledge', 'GET /knowledge-bases/:id/documents/:docId/status', async function () {
          var d = await T.apiGet('/knowledge-bases/' + kbId + '/documents/' + docId + '/status');
          T.assert(d.id === docId, 'doc id mismatch');
          T.assert(typeof d.status === 'string', 'status should be string');
          T.assert(typeof d.filename === 'string', 'filename should be string');
        });

        await T.test('crud', 'knowledge', 'GET /knowledge-bases/:id/documents (has doc)', async function () {
          var d = await T.apiGet('/knowledge-bases/' + kbId + '/documents');
          T.assertHasKey(d, 'data', '/kb-documents');
          T.assert(d.data.length >= 1, 'should have at least 1 document');
        });

        await T.test('crud', 'knowledge', 'DELETE /knowledge-bases/:id/documents/:docId', async function () {
          var token = await T.getAuthToken();
          var resp = await fetch(T.base + '/api/v1/knowledge-bases/' + kbId + '/documents/' + docId, {
            method: 'DELETE',
            headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
            credentials: 'same-origin'
          });
          T.assert(resp.ok, 'delete doc should succeed, got ' + resp.status);
          var body = await resp.json();
          T.assert(body.deleted === true, 'deleted should be true');
          docId = null;
        });

        await T.test('crud', 'knowledge', 'GET /documents/:docId/status (deleted → 404)', async function () {
          var token = await T.getAuthToken();
          var resp = await T.authFetch(token, 'GET',
            '/knowledge-bases/' + kbId + '/documents/00000000-0000-0000-0000-000000000099/status');
          T.assert(resp._status === 404, 'expected 404 for deleted doc, got ' + resp._status);
        });
      }
    }

    // ── Search ──
    await T.test('crud', 'knowledge', 'POST /knowledge-bases/:id/search', async function () {
      try {
        var d = await T.apiPost('/knowledge-bases/' + kbId + '/search', {
          query: 'test', limit: 5
        });
        // Response envelope uses "data" key (not "results")
        T.assertHasKey(d, 'data', '/kb-search');
        T.assertHasKey(d, 'query', '/kb-search');
        T.assertHasKey(d, 'total', '/kb-search');
        T.assert(Array.isArray(d.data), 'data should be array');
      } catch (e) {
        // "failed to embed query" = no embedding model configured (infrastructure, not contract bug)
        if (e.message && e.message.indexOf('embed') !== -1) return;
        throw e;
      }
    });

    // ── Rebuild (no documents = 400, or no ingester = 503) ──
    await T.test('crud', 'knowledge', 'POST /knowledge-bases/:id/rebuild (empty → 400|503)', async function () {
      var token = await T.getAuthToken();
      var resp = await T.authFetch(token, 'POST', '/knowledge-bases/' + kbId + '/rebuild', {});
      T.assert(resp._status === 400 || resp._status === 503,
        'expected 400 (no docs) or 503 (no ingester), got ' + resp._status);
    });

    // ── Channel KB Binding ──
    var bindChannelId = null;
    await T.test('crud', 'knowledge', 'PUT /channels/:id/knowledge-bases (bind)', async function () {
      // Create a temp channel
      var ch = await T.apiPost('/channels', { title: testTag + '-kb-bind' });
      bindChannelId = ch.id;
      T.registerCleanup(function () { if (bindChannelId) return T.safeDelete('/channels/' + bindChannelId); });

      // Bind KB
      var d = await T.apiPut('/channels/' + bindChannelId + '/knowledge-bases', {
        kb_ids: [kbId]
      });
      T.assertHasKey(d, 'data', '/channel-kb-set');
    });

    if (bindChannelId) {
      await T.test('crud', 'knowledge', 'GET /channels/:id/knowledge-bases (verify)', async function () {
        var d = await T.apiGet('/channels/' + bindChannelId + '/knowledge-bases');
        T.assertHasKey(d, 'data', '/channel-kbs');
        T.assert(d.data.length === 1, 'should have 1 linked KB, got ' + d.data.length);
        var linked = d.data[0];
        T.assert(linked.kb_id === kbId, 'linked kb_id mismatch');
        T.assert(typeof linked.kb_name === 'string', 'kb_name should be string');
        T.assert(typeof linked.enabled === 'boolean', 'enabled should be boolean');
      });

      await T.test('crud', 'knowledge', 'PUT /channels/:id/knowledge-bases (unbind)', async function () {
        var d = await T.apiPut('/channels/' + bindChannelId + '/knowledge-bases', { kb_ids: [] });
        T.assertHasKey(d, 'data', '/channel-kb-unbind');
        T.assert(d.data.length === 0, 'should have 0 linked KBs after unbind');
      });
    }

    // ── Discoverable ──
    await T.test('crud', 'knowledge', 'PUT /knowledge-bases/:id/discoverable (toggle)', async function () {
      var d = await T.apiPut('/knowledge-bases/' + kbId + '/discoverable', { discoverable: false });
      T.assert(d.status === 'ok', 'expected status ok');

      // Toggle back
      d = await T.apiPut('/knowledge-bases/' + kbId + '/discoverable', { discoverable: true });
      T.assert(d.status === 'ok', 'expected status ok on re-enable');
    });

    // ── Cross-user isolation (requires fixtures) ──
    var fixtureUser = T.getFixtureUser ? T.getFixtureUser('-user') : null;
    if (fixtureUser && fixtureUser.token) {
      await T.test('crud', 'knowledge', 'isolation: other user cannot access personal KB', async function () {
        var resp = await T.authFetch(fixtureUser.token, 'GET', '/knowledge-bases/' + kbId);
        // Personal KB of admin should be hidden (404) from other user
        T.assert(resp._status === 404, 'expected 404 for other user, got ' + resp._status);
      });

      await T.test('crud', 'knowledge', 'isolation: other user cannot delete personal KB', async function () {
        var resp = await T.authFetch(fixtureUser.token, 'DELETE', '/knowledge-bases/' + kbId);
        T.assert(resp._status === 404, 'expected 404 for delete by other user, got ' + resp._status);
      });

      await T.test('crud', 'knowledge', 'scope: non-admin global create → 403', async function () {
        var resp = await T.authFetch(fixtureUser.token, 'POST', '/knowledge-bases', {
          name: testTag + '-sneaky-global', scope: 'global'
        });
        T.assert(resp._status === 403, 'expected 403 for non-admin global KB, got ' + resp._status);
      });

      await T.test('crud', 'knowledge', 'discoverable: other user cannot toggle', async function () {
        var resp = await T.authFetch(fixtureUser.token, 'PUT',
          '/knowledge-bases/' + kbId + '/discoverable', { discoverable: false });
        T.assert(resp._status === 404 || resp._status === 403,
          'expected 403/404 for non-owner toggle, got ' + resp._status);
      });
    }

    // ── Delete ──
    await T.test('crud', 'knowledge', 'DELETE /knowledge-bases/:id', async function () {
      await T.safeDelete('/knowledge-bases/' + kbId);
      kbId = null;
    });

    await T.test('crud', 'knowledge', 'GET /knowledge-bases/:id (deleted → 404)', async function () {
      var token = await T.getAuthToken();
      var resp = await T.authFetch(token, 'GET', '/knowledge-bases/00000000-0000-0000-0000-000000000099');
      T.assert(resp._status === 404, 'expected 404 for nonexistent KB, got ' + resp._status);
    });

    // Cleanup temp channel
    if (bindChannelId) {
      try { await T.safeDelete('/channels/' + bindChannelId); } catch (e) { /* ok */ }
      bindChannelId = null;
    }

  };
})();
