/**
 * ICD Test Runner — CRUD: Projects
 * Full project lifecycle — CRUD, channel/KB/note associations, files,
 * isolation, and admin list.
 */
(function () {
  'use strict';
  var T = window.ICD;
  if (!T) return;
  if (!T.crud) T.crud = {};

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

    // ── Create ──
    var projectId = null;
    await T.test('crud', 'projects', 'POST /projects (create, 201 + shape)', async function () {
      var token = await T.getAuthToken();
      var d = await T.authFetch(token, 'POST', '/projects', {
        name: testTag + '-project',
        description: 'ICD integration test project',
        color: '#3b82f6',
        icon: 'folder'
      });
      T.assertStatus(d, 201, 'POST /projects');
      T.assertShape(d, T.S.project, 'project');
      T.assert(d.scope === 'personal', 'scope forced to personal, got ' + d.scope);
      T.assert(typeof d.owner_id === 'string' && d.owner_id.length > 0, 'owner_id must be set');
      projectId = d.id;
      T.registerCleanup(function () { if (projectId) return T.safeDelete('/projects/' + projectId); });
    });

    if (!projectId) return;

    // ── Read ──
    await T.test('crud', 'projects', 'GET /projects/:id (read, shape)', async function () {
      var d = await T.apiGet('/projects/' + projectId);
      T.assertShape(d, T.S.project, 'project');
      T.assert(d.id === projectId, 'id mismatch');
      T.assert(d.name === testTag + '-project', 'name mismatch');
      T.assert(d.description === 'ICD integration test project', 'description mismatch');
    });

    // ── Update ──
    await T.test('crud', 'projects', 'PUT /projects/:id (update, returns refreshed)', async function () {
      var d = await T.apiPut('/projects/' + projectId, { name: testTag + '-proj-updated', is_archived: false });
      T.assertShape(d, T.S.project, 'project');
      T.assert(d.name === testTag + '-proj-updated', 'name not updated');
      T.assert(d.id === projectId, 'id changed unexpectedly');
    });

    // ── List envelope ──
    await T.test('crud', 'projects', 'GET /projects (list envelope)', async function () {
      var d = await T.apiGet('/projects');
      T.assertHasKey(d, 'data', '/projects');
      T.assert(Array.isArray(d.data), 'data must be array');
      var found = d.data.some(function (p) { return p.id === projectId; });
      T.assert(found, 'created project not in list');
    });

    // ── Channel association ──
    var channelId = null;
    await T.test('crud', 'projects', 'POST /projects/:id/channels (add channel)', async function () {
      // Create a scratch channel for association testing
      var ch = await T.apiPost('/channels', { title: testTag + '-proj-ch', type: 'direct' });
      channelId = ch.id;
      T.registerCleanup(function () { if (channelId) return T.safeDelete('/channels/' + channelId); });

      var d = await T.apiPost('/projects/' + projectId + '/channels', { channel_id: channelId, position: 0 });
      T.assert(typeof d === 'object', 'expected object response');
    });

    await T.test('crud', 'projects', 'GET /projects/:id/channels (list, has added)', async function () {
      var d = await T.apiGet('/projects/' + projectId + '/channels');
      T.assertHasKey(d, 'data', '/project-channels');
      T.assert(Array.isArray(d.data), 'data must be array');
      T.assert(d.data.length >= 1, 'expected at least 1 channel association');
      var entry = d.data[0];
      T.assert(typeof entry.channel_id === 'string', 'missing channel_id');
      T.assert(typeof entry.project_id === 'string', 'missing project_id');
      T.assert(typeof entry.added_at === 'string', 'missing added_at');
    });

    await T.test('crud', 'projects', 'DELETE /projects/:id/channels/:channelId (remove)', async function () {
      await T.apiDelete('/projects/' + projectId + '/channels/' + channelId);
      var d = await T.apiGet('/projects/' + projectId + '/channels');
      T.assert(d.data.length === 0, 'channel should be removed');
    });

    // ── KB association ──
    var kbId = null;
    await T.test('crud', 'projects', 'POST /projects/:id/knowledge-bases (add KB)', async function () {
      // Create a scratch KB
      var kb = await T.apiPost('/knowledge-bases', { name: testTag + '-proj-kb', scope: 'personal' });
      kbId = kb.id;
      T.registerCleanup(function () { if (kbId) return T.safeDelete('/knowledge-bases/' + kbId); });

      var d = await T.apiPost('/projects/' + projectId + '/knowledge-bases', { kb_id: kbId, auto_search: true });
      T.assert(typeof d === 'object', 'expected object response');
    });

    await T.test('crud', 'projects', 'GET /projects/:id/knowledge-bases (list, has added)', async function () {
      var d = await T.apiGet('/projects/' + projectId + '/knowledge-bases');
      T.assertHasKey(d, 'data', '/project-kbs');
      T.assert(d.data.length >= 1, 'expected at least 1 KB association');
      var entry = d.data[0];
      T.assert(typeof entry.kb_id === 'string', 'missing kb_id');
      T.assert(typeof entry.project_id === 'string', 'missing project_id');
      T.assert(typeof entry.added_at === 'string', 'missing added_at');
    });

    await T.test('crud', 'projects', 'DELETE /projects/:id/knowledge-bases/:kbId (remove)', async function () {
      await T.apiDelete('/projects/' + projectId + '/knowledge-bases/' + kbId);
      var d = await T.apiGet('/projects/' + projectId + '/knowledge-bases');
      T.assert(d.data.length === 0, 'KB should be removed');
    });

    // ── Note association ──
    var noteId = null;
    await T.test('crud', 'projects', 'POST /projects/:id/notes (add note)', async function () {
      // Create a scratch note
      var n = await T.apiPost('/notes', { title: testTag + '-proj-note', content: 'test' });
      noteId = n.id;
      T.registerCleanup(function () { if (noteId) return T.safeDelete('/notes/' + noteId); });

      var d = await T.apiPost('/projects/' + projectId + '/notes', { note_id: noteId });
      T.assert(typeof d === 'object', 'expected object response');
    });

    await T.test('crud', 'projects', 'GET /projects/:id/notes (list, has added)', async function () {
      var d = await T.apiGet('/projects/' + projectId + '/notes');
      T.assertHasKey(d, 'data', '/project-notes');
      T.assert(d.data.length >= 1, 'expected at least 1 note association');
      var entry = d.data[0];
      T.assert(typeof entry.note_id === 'string', 'missing note_id');
      T.assert(typeof entry.project_id === 'string', 'missing project_id');
      T.assert(typeof entry.added_at === 'string', 'missing added_at');
    });

    await T.test('crud', 'projects', 'DELETE /projects/:id/notes/:noteId (remove)', async function () {
      await T.apiDelete('/projects/' + projectId + '/notes/' + noteId);
      var d = await T.apiGet('/projects/' + projectId + '/notes');
      T.assert(d.data.length === 0, 'note should be removed');
    });

    // ── Files (shape only — no upload in runner) ──
    await T.test('crud', 'projects', 'GET /projects/:id/files (files key + count)', async function () {
      var d = await T.apiGet('/projects/' + projectId + '/files');
      T.assertHasKey(d, 'files', '/project-files');
      T.assertHasKey(d, 'count', '/project-files');
      T.assert(Array.isArray(d.files), 'files must be array');
      T.assert(typeof d.count === 'number', 'count must be number');
    });

    // ── Isolation ──
    await T.test('crud', 'projects', 'isolation: other user cannot GET personal project', async function () {
      var other = T.getFixtureUser('-user');
      if (!other || !other.token) { T.assert(true, 'skip — no fixture user'); return; }
      var d = await T.authFetch(other.token, 'GET', '/projects/' + projectId);
      T.assert(d._status === 404, 'expected 404 for other user, got ' + d._status);
    });

    await T.test('crud', 'projects', 'isolation: other user cannot DELETE personal project', async function () {
      var other = T.getFixtureUser('-user');
      if (!other || !other.token) { T.assert(true, 'skip — no fixture user'); return; }
      var d = await T.authFetch(other.token, 'DELETE', '/projects/' + projectId);
      T.assert(d._status === 404, 'expected 404 for other user DELETE, got ' + d._status);
    });

    // ── Admin list ──
    await T.test('crud', 'projects', 'GET /admin/projects (envelope + owner_name)', async function () {
      var d = await T.apiGet('/admin/projects');
      T.assertHasKey(d, 'data', '/admin/projects');
      T.assert(Array.isArray(d.data), 'data must be array');
      var found = d.data.find(function (p) { return p.id === projectId; });
      T.assert(found, 'created project not in admin list');
      T.assert(typeof found.owner_name === 'string', 'admin list must include owner_name');
      T.assert(typeof found.channel_count === 'number', 'admin list must include channel_count');
      T.assert(typeof found.kb_count === 'number', 'admin list must include kb_count');
      T.assert(typeof found.note_count === 'number', 'admin list must include note_count');
    });

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

    // Cleanup scratch resources (cleanups run in reverse but let's be explicit)
    if (channelId) { try { await T.safeDelete('/channels/' + channelId); channelId = null; } catch (e) { /* ok */ } }
    if (kbId) { try { await T.safeDelete('/knowledge-bases/' + kbId); kbId = null; } catch (e) { /* ok */ } }
    if (noteId) { try { await T.safeDelete('/notes/' + noteId); noteId = null; } catch (e) { /* ok */ } }

  };
})();
