/**
 * ICD Test Runner — CRUD: Channels
 * Channel lifecycle, DMs, folders, participants, model roster,
 * KB linking, files, messages, typing, mark-read, user search.
 */
(function () {
  'use strict';
  var T = window.ICD;
  if (!T) return;
  if (!T.crud) T.crud = {};

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

    // ── Channels CRUD ──
    var channelId = null;
    await T.test('crud', 'channels', 'POST /channels (create)', async function () {
      var d = await T.apiPost('/channels', {
        title: testTag + '-channel',
        type: 'direct',
        description: 'ICD integration test channel',
        tags: ['icd-test']
      });
      T.assertShape(d, T.S.channelFull, 'created channel');
      T.assert(d.type === 'direct', 'type should be direct');
      T.assert(d.is_archived === false, 'should not be archived');
      channelId = d.id;
      T.registerCleanup(function () { if (channelId) return T.safeDelete('/channels/' + channelId); });
    });

    if (channelId) {
      await T.test('crud', 'channels', 'GET /channels/:id (read)', async function () {
        var d = await T.apiGet('/channels/' + channelId);
        T.assertShape(d, T.S.channelFull, 'channel');
        T.assert(d.id === channelId, 'id mismatch');
        T.assert(d.title.indexOf(testTag) !== -1, 'title mismatch');
      });

      await T.test('crud', 'channels', 'PUT /channels/:id (update title+topic+ai_mode)', async function () {
        var d = await T.apiPut('/channels/' + channelId, {
          title: testTag + '-updated',
          topic: 'ICD test topic',
          ai_mode: 'mention_only'
        });
        T.assert(d.title === testTag + '-updated', 'title not updated');
        T.assert(d.topic === 'ICD test topic', 'topic not set');
        T.assert(d.ai_mode === 'mention_only', 'ai_mode not set');
      });

      await T.test('crud', 'channels', 'PUT /channels/:id (restore ai_mode)', async function () {
        var d = await T.apiPut('/channels/' + channelId, { ai_mode: 'auto' });
        T.assert(d.ai_mode === 'auto', 'ai_mode not restored');
      });

      // ── Channel List Query Filters ──
      await T.test('crud', 'channels', 'GET /channels?type=direct', async function () {
        var d = await T.apiGet('/channels?type=direct&per_page=5');
        var arr = d.data || [];
        T.assert(Array.isArray(arr), 'expected data array');
        arr.forEach(function (ch) { T.assert(ch.type === 'direct', 'type filter leaked: ' + ch.type); });
      });

      await T.test('crud', 'channels', 'GET /channels?search=...', async function () {
        var d = await T.apiGet('/channels?search=' + encodeURIComponent(testTag) + '&per_page=5');
        var arr = d.data || [];
        T.assert(Array.isArray(arr), 'expected data array');
        T.assert(arr.length >= 1, 'search should find our channel');
      });

      await T.test('crud', 'channels', 'GET /channels?archived=true (empty)', async function () {
        var d = await T.apiGet('/channels?archived=true&per_page=5');
        var arr = d.data || [];
        T.assert(Array.isArray(arr), 'expected data array');
        // Our channel is not archived, so it shouldn't appear here
      });

      // ── Message Tree ──
      await T.test('crud', 'channels', 'GET /channels/:id/path (empty)', async function () {
        var d = await T.apiGet('/channels/' + channelId + '/path');
        T.assertHasKey(d, 'messages', '/path');
        T.assert(Array.isArray(d.messages), 'messages should be array');
      });

      // ── Participants ──
      await T.test('crud', 'channels', 'GET /channels/:id/participants (auto owner)', async function () {
        var d = await T.apiGet('/channels/' + channelId + '/participants');
        T.assertHasKey(d, 'participants', '/participants');
        T.assert(Array.isArray(d.participants), 'participants should be array');
        T.assert(d.participants.length >= 1, 'should have at least owner participant');
        if (d.participants.length > 0) {
          T.assertShape(d.participants[0], T.S.participant, 'participant[0]');
          T.assert(d.participants[0].role === 'owner', 'first participant should be owner');
        }
      });

      // ── Participant CRUD (add fixture user, update role, remove) ──
      var addedParticipantId = null;
      var fixtureUser = T.getFixtureUser ? T.getFixtureUser('-user') : null;
      if (fixtureUser && fixtureUser.id) {
        await T.test('crud', 'channels', 'POST /channels/:id/participants (add user)', async function () {
          var d = await T.apiPost('/channels/' + channelId + '/participants', {
            participant_type: 'user',
            participant_id: fixtureUser.id,
            role: 'member'
          });
          // Response varies — may be participant object or list
          var p = d.participant || d;
          T.assert(p.id || p.participants, 'expected participant id or list');
          if (p.id) addedParticipantId = p.id;
          // Verify by re-listing
          var list = await T.apiGet('/channels/' + channelId + '/participants');
          var found = list.participants.find(function (pp) { return pp.participant_id === fixtureUser.id; });
          T.assert(found, 'added user not found in participant list');
          if (!addedParticipantId && found) addedParticipantId = found.id;
        });

        if (addedParticipantId) {
          await T.test('crud', 'channels', 'PATCH /channels/:id/participants/:id (update role)', async function () {
            var d = await T.apiPatch('/channels/' + channelId + '/participants/' + addedParticipantId, {
              role: 'observer'
            });
            T.assert(typeof d === 'object', 'expected response');
            // Verify
            var list = await T.apiGet('/channels/' + channelId + '/participants');
            var found = list.participants.find(function (pp) { return pp.id === addedParticipantId; });
            T.assert(found && found.role === 'observer', 'role not updated to observer');
          });

          // ── Multi-user access: participant can GET channel (CS1 fix) ──
          await T.test('crud', 'channels', 'GET /channels/:id (as participant, CS1 fix)', async function () {
            var d = await T.authFetch(fixtureUser.token, 'GET', '/channels/' + channelId);
            T.assert(d._status === 200, 'participant should see channel, got ' + d._status);
            T.assert(d.id === channelId, 'id mismatch');
          });

          await T.test('crud', 'channels', 'GET /channels/:id/path (as participant)', async function () {
            var d = await T.authFetch(fixtureUser.token, 'GET', '/channels/' + channelId + '/path');
            T.assert(d._status === 200, 'participant should see path, got ' + d._status);
            T.assertHasKey(d, 'messages', 'participant path');
          });

          await T.test('crud', 'channels', 'DELETE /channels/:id/participants/:id (remove)', async function () {
            await T.apiDelete('/channels/' + channelId + '/participants/' + addedParticipantId);
            var list = await T.apiGet('/channels/' + channelId + '/participants');
            var found = list.participants.find(function (pp) { return pp.id === addedParticipantId; });
            T.assert(!found, 'participant should be removed');
            addedParticipantId = null;
          });
        }
      }

      // ── Model Roster CRUD ──
      await T.test('crud', 'channels', 'GET /channels/:id/models (roster)', async function () {
        var d = await T.apiGet('/channels/' + channelId + '/models');
        T.assertHasKey(d, 'models', '/channel-models');
        T.assert(Array.isArray(d.models), 'models should be array');
      });

      var rosterModelId = null;
      await T.test('crud', 'channels', 'POST /channels/:id/models (add)', async function () {
        var d = await T.apiPost('/channels/' + channelId + '/models', {
          model_id: 'icd-test-model',
          display_name: 'ICD Test Model',
          provider_config_id: null
        });
        T.assertHasKey(d, 'models', '/models add');
        T.assert(Array.isArray(d.models), 'models should be array');
        var added = d.models.find(function (m) { return m.model_id === 'icd-test-model'; });
        T.assert(added, 'added model not found in roster');
        rosterModelId = added ? added.id : null;
      });

      if (rosterModelId) {
        await T.test('crud', 'channels', 'PATCH /channels/:id/models/:id (update)', async function () {
          var d = await T.apiPatch('/channels/' + channelId + '/models/' + rosterModelId, {
            display_name: 'ICD Updated Model'
          });
          T.assertHasKey(d, 'models', '/models update');
          var updated = d.models.find(function (m) { return m.id === rosterModelId; });
          T.assert(updated && updated.display_name === 'ICD Updated Model', 'display_name not updated');
        });

        await T.test('crud', 'channels', 'DELETE /channels/:id/models/:id (remove)', async function () {
          var d = await T.apiDelete('/channels/' + channelId + '/models/' + rosterModelId);
          T.assertHasKey(d, 'models', '/models delete');
          var models = d.models || [];
          var found = models.find(function (m) { return m.id === rosterModelId; });
          T.assert(!found, 'deleted model still in roster');
          rosterModelId = null;
        });
      }

      // ── KB linking ──
      await T.test('crud', 'channels', 'GET /channels/:id/knowledge-bases', async function () {
        var d = await T.apiGet('/channels/' + channelId + '/knowledge-bases');
        T.assertHasKey(d, 'data', '/channel-kbs');
      });

      // ── Files ──
      await T.test('crud', 'channels', 'GET /channels/:id/files', async function () {
        var d = await T.apiGet('/channels/' + channelId + '/files');
        T.assertHasKey(d, 'files', '/channel-files');
        T.assert(Array.isArray(d.files), 'files should be array');
      });

      // ── File Upload/Download Lifecycle (conditional on storage) ──
      var storageOk = false;
      if (T.user.role === 'admin') {
        try {
          var ss = await T.apiGet('/admin/storage/status');
          storageOk = ss && ss.configured === true;
        } catch (e) { /* not admin or storage disabled */ }
      }

      var uploadedFileId = null;
      if (storageOk) {
        await T.test('crud', 'channels', 'POST /channels/:id/files (upload)', async function () {
          var blob = new Blob(['ICD test file content'], { type: 'text/plain' });
          var d = await T.apiUpload('/channels/' + channelId + '/files', blob, 'icd-test.txt');
          T.assert(d.id, 'expected file id');
          T.assert(d.filename === 'icd-test.txt', 'filename mismatch');
          T.assert(d.origin === 'user_upload', 'origin should be user_upload');
          T.assert(d.content_type === 'text/plain', 'content_type mismatch');
          uploadedFileId = d.id;
          T.registerCleanup(function () { if (uploadedFileId) return T.safeDelete('/files/' + uploadedFileId); });
        });

        if (uploadedFileId) {
          await T.test('crud', 'channels', 'GET /files/:id (metadata)', async function () {
            var d = await T.apiGet('/files/' + uploadedFileId);
            T.assertShape(d, T.S.file, 'file metadata');
            T.assert(d.id === uploadedFileId, 'id mismatch');
            T.assert(d.channel_id === channelId, 'channel_id mismatch');
          });

          await T.test('crud', 'channels', 'GET /files/:id/download', async function () {
            var token = await T.getAuthToken();
            var resp = await fetch(T.base + '/api/v1/files/' + uploadedFileId + '/download', {
              headers: { 'Authorization': 'Bearer ' + token },
              credentials: 'same-origin'
            });
            T.assert(resp.ok, 'download should succeed, got ' + resp.status);
            var text = await resp.text();
            T.assert(text === 'ICD test file content', 'download content mismatch');
          });

          await T.test('crud', 'channels', 'DELETE /files/:id', async function () {
            var delId = uploadedFileId;
            await T.safeDelete('/files/' + delId);
            uploadedFileId = null;
            // Verify gone
            try {
              await T.apiGet('/files/' + delId);
              T.assert(false, 'file should be deleted');
            } catch (e) {
              T.assert(e.message.indexOf('404') !== -1 || e.message.indexOf('not found') !== -1, 'expected 404');
            }
          });
        }
      }

      // ── Message Create + Edit + Cursor + Siblings ──
      var msgId = null;
      await T.test('crud', 'channels', 'POST /channels/:id/messages (create)', async function () {
        var d = await T.apiPost('/channels/' + channelId + '/messages', {
          role: 'user',
          content: 'ICD integration test message'
        });
        T.assertShape(d, T.S.message, 'message');
        msgId = d.id;
      });

      if (msgId) {
        await T.test('crud', 'channels', 'GET /channels/:id/messages (all)', async function () {
          var d = await T.apiGet('/channels/' + channelId + '/messages');
          var arr = d.messages || d.data;
          T.assert(Array.isArray(arr), 'expected array');
          T.assert(arr.length >= 1, 'expected at least 1 message');
        });

        await T.test('crud', 'channels', 'GET /channels/:id/messages/:id/siblings', async function () {
          var d = await T.apiGet('/channels/' + channelId + '/messages/' + msgId + '/siblings');
          T.assertHasKey(d, 'siblings', '/siblings');
          T.assert(Array.isArray(d.siblings), 'siblings should be array');
        });

        // Edit creates sibling
        var editedMsgId = null;
        await T.test('crud', 'channels', 'POST /channels/:id/messages/:id/edit', async function () {
          var d = await T.apiPost('/channels/' + channelId + '/messages/' + msgId + '/edit', {
            content: 'ICD edited message content'
          });
          T.assertShape(d, T.S.message, 'edited message');
          T.assert(d.id !== msgId, 'edit should create new message id');
          T.assert(d.content === 'ICD edited message content', 'content mismatch');
          editedMsgId = d.id;
        });

        if (editedMsgId) {
          await T.test('crud', 'channels', 'GET siblings (after edit, expect 2)', async function () {
            var d = await T.apiGet('/channels/' + channelId + '/messages/' + msgId + '/siblings');
            T.assert(d.siblings.length >= 2, 'edit should create sibling, got ' + d.siblings.length);
          });

          await T.test('crud', 'channels', 'PUT /channels/:id/cursor (switch branch)', async function () {
            var d = await T.apiPut('/channels/' + channelId + '/cursor', {
              active_leaf_id: msgId
            });
            T.assertHasKey(d, 'messages', '/cursor');
            T.assert(Array.isArray(d.messages), 'cursor response should have messages array');
          });
        }

        await T.test('crud', 'channels', 'GET /messages/:id/files', async function () {
          var d = await T.apiGet('/messages/' + msgId + '/files');
          T.assertHasKey(d, 'files', '/message-files');
          T.assert(Array.isArray(d.files), 'files should be array');
        });

        await T.test('crud', 'channels', 'GET /channels/:id/path (after messages)', async function () {
          var d = await T.apiGet('/channels/' + channelId + '/path');
          T.assertHasKey(d, 'messages', '/path');
          T.assert(d.messages.length >= 1, 'path should have messages');
        });

        await T.test('crud', 'channels', 'POST /channels/:id/generate-title', async function () {
          try {
            var d = await T.apiPost('/channels/' + channelId + '/generate-title', {});
            T.assert(typeof d === 'object', 'expected object');
          } catch (e) {
            if (e.message && (e.message.indexOf('400') !== -1 || e.message.indexOf('502') !== -1 || e.message.indexOf('model') !== -1))
              return;
            throw e;
          }
        });
      }

      // ── Typing indicator ──
      await T.test('crud', 'channels', 'POST /channels/:id/typing', async function () {
        var d = await T.apiPost('/channels/' + channelId + '/typing', {});
        T.assert(d.ok === true, 'expected { ok: true }');
      });

      // ── Mark Read ──
      await T.test('crud', 'channels', 'POST /channels/:id/mark-read', async function () {
        var d = await T.apiPost('/channels/' + channelId + '/mark-read', {});
        T.assert(typeof d === 'object', 'expected object');
      });

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

    // ── DM Creation + Dedup ──
    var dmChannelId = null;
    var dmUser = T.getFixtureUser ? T.getFixtureUser('-user') : null;
    if (dmUser && dmUser.id) {
      await T.test('crud', 'channels', 'POST /channels (type=dm, create)', async function () {
        var d = await T.apiPost('/channels', {
          title: testTag + '-dm',
          type: 'dm',
          participants: [dmUser.id]
        });
        T.assert(d.id, 'DM channel should have id');
        T.assert(d.type === 'dm', 'type should be dm');
        dmChannelId = d.id;
        T.registerCleanup(function () { if (dmChannelId) return T.safeDelete('/channels/' + dmChannelId); });
      });

      if (dmChannelId) {
        await T.test('crud', 'channels', 'POST /channels (dm dedup → 200)', async function () {
          var d = await T.apiPost('/channels', {
            title: testTag + '-dm-dup',
            type: 'dm',
            participants: [dmUser.id]
          });
          T.assert(d.id === dmChannelId, 'dedup should return same channel id');
        });

        await T.test('crud', 'channels', 'GET /channels?types=dm (type filter)', async function () {
          var d = await T.apiGet('/channels?types=dm&per_page=10');
          var arr = d.data || [];
          T.assert(Array.isArray(arr), 'expected data array');
          var found = arr.find(function (ch) { return ch.id === dmChannelId; });
          T.assert(found, 'DM channel should appear in dm type filter');
        });

        await T.test('crud', 'channels', 'DELETE /channels/:id (dm cleanup)', async function () {
          await T.safeDelete('/channels/' + dmChannelId);
          dmChannelId = null;
        });
      }
    }

    // ── Channel Types (group, channel) ──
    var groupChId = null;
    await T.test('crud', 'channels', 'POST /channels (type=group)', async function () {
      var d = await T.apiPost('/channels', {
        title: testTag + '-group',
        type: 'group',
        description: 'ICD group test'
      });
      T.assertShape(d, T.S.channelFull, 'group channel');
      T.assert(d.type === 'group', 'type should be group, got: ' + d.type);
      groupChId = d.id;
      T.registerCleanup(function () { if (groupChId) return T.safeDelete('/channels/' + groupChId); });
    });

    var teamChId = null;
    await T.test('crud', 'channels', 'POST /channels (type=channel)', async function () {
      var d = await T.apiPost('/channels', {
        title: testTag + '-team-channel',
        type: 'channel',
        description: 'ICD channel test'
      });
      T.assertShape(d, T.S.channelFull, 'team channel');
      T.assert(d.type === 'channel', 'type should be channel, got: ' + d.type);
      teamChId = d.id;
      T.registerCleanup(function () { if (teamChId) return T.safeDelete('/channels/' + teamChId); });
    });

    // ── Multi-type filter ──
    await T.test('crud', 'channels', 'GET /channels?types=group,channel (multi)', async function () {
      var d = await T.apiGet('/channels?types=group,channel&per_page=50');
      var arr = d.data || [];
      T.assert(Array.isArray(arr), 'expected data array');
      arr.forEach(function (ch) {
        T.assert(ch.type === 'group' || ch.type === 'channel',
          'multi-type filter leaked: ' + ch.type);
      });
    });

    await T.test('crud', 'channels', 'GET /channels?types=direct,dm,group,channel (all)', async function () {
      var d = await T.apiGet('/channels?types=direct,dm,group,channel&per_page=50');
      var arr = d.data || [];
      T.assert(Array.isArray(arr), 'expected data array');
      var types = new Set(arr.map(function (ch) { return ch.type; }));
      T.assert(types.size <= 4, 'should only contain known types');
    });

    // Cleanup channel types
    if (groupChId) {
      await T.test('crud', 'channels', 'DELETE /channels/:id (group cleanup)', async function () {
        await T.safeDelete('/channels/' + groupChId);
        groupChId = null;
      });
    }
    if (teamChId) {
      await T.test('crud', 'channels', 'DELETE /channels/:id (channel cleanup)', async function () {
        await T.safeDelete('/channels/' + teamChId);
        teamChId = null;
      });
    }

    // ── Folders CRUD + Channel Assignment ──
    var folderId = null;
    await T.test('crud', 'channels', 'POST /folders (create)', async function () {
      var d = await T.apiPost('/folders', { name: testTag + '-folder', sort_order: 0 });
      var folder = d.folder || d.data || d;
      T.assert(folder.id || folder.ID, 'folder response missing id, got keys: ' + Object.keys(folder).join(', '));
      folderId = folder.id || folder.ID;
      T.registerCleanup(function () { if (folderId) return T.safeDelete('/folders/' + folderId); });
    });

    if (folderId) {
      await T.test('crud', 'channels', 'PUT /folders/:id (update)', async function () {
        var d = await T.apiPut('/folders/' + folderId, { name: testTag + '-folder-updated', sort_order: 1 });
        T.assert(typeof d === 'object', 'expected object');
      });

      // Create a channel, assign to folder, verify filter works
      var folderCh = null;
      await T.test('crud', 'channels', 'PUT /channels/:id { folder_id } (assign)', async function () {
        var ch = await T.apiPost('/channels', { title: testTag + '-folder-test', type: 'direct' });
        folderCh = ch.id;
        T.registerCleanup(function () { if (folderCh) return T.safeDelete('/channels/' + folderCh); });
        var d = await T.apiPut('/channels/' + folderCh, { folder_id: folderId });
        T.assert(d.folder_id === folderId, 'folder_id should be set, got: ' + d.folder_id);
      });

      if (folderCh) {
        await T.test('crud', 'channels', 'GET /channels?folder_id=... (filter)', async function () {
          var d = await T.apiGet('/channels?folder_id=' + folderId + '&per_page=10');
          var arr = d.data || [];
          T.assert(Array.isArray(arr), 'expected data array');
          var found = arr.find(function (ch) { return ch.id === folderCh; });
          T.assert(found, 'channel should appear in folder_id filter');
        });

        await T.test('crud', 'channels', 'PUT /channels/:id { folder_id: "" } (unbind)', async function () {
          var d = await T.apiPut('/channels/' + folderCh, { folder_id: '' });
          T.assert(!d.folder_id, 'folder_id should be cleared');
        });

        await T.test('crud', 'channels', 'DELETE /channels/:id (folder test cleanup)', async function () {
          await T.safeDelete('/channels/' + folderCh);
          folderCh = null;
        });
      }

      await T.test('crud', 'channels', 'DELETE /folders/:id', async function () {
        await T.safeDelete('/folders/' + folderId);
        folderId = null;
      });
    }

    // ── User Search ──
    await T.test('crud', 'channels', 'GET /users/search', async function () {
      var d = await T.apiGet('/users/search?q=' + encodeURIComponent(T.user.username.substring(0, 3)));
      T.assertHasKey(d, 'users', '/users/search');
      T.assert(Array.isArray(d.users), 'users should be array');
    });

  };
})();
