/**
 * Tests for the activity feed query helper.
 *
 * Each test initializes an in-memory PGlite instance, creates the schema,
 * exercises getActivityFeed, and tears down the database.
 */

import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test';
import { setupTestDb, cleanTestDb, teardownTestDb } from '../../helpers/db';
import { upsertSession } from '@myco/db/queries/sessions.js';
import { registerAgent } from '@myco/db/queries/agents.js';
import { insertRun } from '@myco/db/queries/runs.js';
import { insertSpore } from '@myco/db/queries/spores.js';
import { getActivityFeed } from '@myco/db/queries/feed.js';
import { ALL_PROJECTS_SCOPE, assertGroveProjectId, projectScope } from '@myco/grove/ids.js';

/** Epoch seconds helper. */
const epochNow = () => Math.floor(Date.now() / 1000);

/** Shared agent ID used across tests. */
const TEST_AGENT_ID = 'agent-feed-test';

describe('getActivityFeed', () => {
  beforeAll(() => { setupTestDb(); });
  afterAll(() => { teardownTestDb(); });
  beforeEach(async () => {
    cleanTestDb();
    // Insert agent required as FK for agent_runs and spores
    registerAgent({
      id: TEST_AGENT_ID,
      name: 'Feed Test Agent',
      created_at: epochNow(),
    });
  });

  // ---------------------------------------------------------------------------
  // Empty feed
  // ---------------------------------------------------------------------------

  it('returns empty array when no data exists', async () => {
    const feed = getActivityFeed(ALL_PROJECTS_SCOPE);
    expect(feed).toEqual([]);
  });

  // ---------------------------------------------------------------------------
  // Unified feed sorted by timestamp
  // ---------------------------------------------------------------------------

  it('returns entries from all three tables merged and sorted by timestamp DESC', async () => {
    const base = epochNow();

    // Session at base+30 (newest)
    upsertSession({
      id: 'sess-feed-1',
      agent: 'claude-code',
      started_at: base + 30,
      created_at: base + 30,
      title: 'Feed Session',
    });

    // Agent run at base+20
    insertRun({
      id: 'run-feed-1',
      agent_id: TEST_AGENT_ID,
      task: 'curate spores',
      status: 'completed',
      started_at: base + 20,
      completed_at: base + 20,
    });

    // Spore at base+10 (oldest)
    insertSpore({
      id: 'spore-feed-1',
      agent_id: TEST_AGENT_ID,
      observation_type: 'gotcha',
      content: 'Watch out for PGlite file locks',
      created_at: base + 10,
    });

    const feed = getActivityFeed(ALL_PROJECTS_SCOPE);

    expect(feed).toHaveLength(3);

    // First entry should be the session (highest timestamp)
    expect(feed[0].type).toBe('session');
    expect(feed[0].id).toBe('sess-feed-1');
    expect(feed[0].summary).toBe('Feed Session');
    expect(feed[0].timestamp).toBe(base + 30);

    // Second entry should be the agent run
    expect(feed[1].type).toBe('agent_run');
    expect(feed[1].id).toBe('run-feed-1');
    expect(feed[1].summary).toBe('curate spores — completed');
    expect(feed[1].timestamp).toBe(base + 20);

    // Third entry should be the spore
    expect(feed[2].type).toBe('spore');
    expect(feed[2].id).toBe('spore-feed-1');
    expect(feed[2].summary).toContain('gotcha:');
    expect(feed[2].summary).toContain('Watch out for PGlite file locks');
    expect(feed[2].timestamp).toBe(base + 10);
  });

  // ---------------------------------------------------------------------------
  // Session fallback title
  // ---------------------------------------------------------------------------

  it('uses fallback title for sessions without a title', async () => {
    const now = epochNow();
    const sessionId = 'abcdefgh-1234-5678-abcd-ef0123456789';

    upsertSession({
      id: sessionId,
      agent: 'claude-code',
      started_at: now,
      created_at: now,
      title: null,
    });

    const feed = getActivityFeed(ALL_PROJECTS_SCOPE);
    expect(feed).toHaveLength(1);
    expect(feed[0].summary).toBe('Session abcdefgh');
  });

  // ---------------------------------------------------------------------------
  // Spore content truncation to 80 chars
  // ---------------------------------------------------------------------------

  it('truncates long spore content to 80 chars in summary', async () => {
    const now = epochNow();
    const longContent = 'A'.repeat(200);

    insertSpore({
      id: 'spore-long',
      agent_id: TEST_AGENT_ID,
      observation_type: 'decision',
      content: longContent,
      created_at: now,
    });

    const feed = getActivityFeed(ALL_PROJECTS_SCOPE);
    expect(feed).toHaveLength(1);
    // summary = "decision: " + LEFT(content, 80) = "decision: " + 80 A's
    expect(feed[0].summary).toBe('decision: ' + 'A'.repeat(80));
  });

  // ---------------------------------------------------------------------------
  // Spores with non-active status excluded
  // ---------------------------------------------------------------------------

  it('excludes spores with non-active status', async () => {
    const now = epochNow();

    insertSpore({
      id: 'spore-active',
      agent_id: TEST_AGENT_ID,
      observation_type: 'gotcha',
      content: 'Active spore',
      created_at: now,
      status: 'active',
    });

    insertSpore({
      id: 'spore-superseded',
      agent_id: TEST_AGENT_ID,
      observation_type: 'gotcha',
      content: 'Superseded spore',
      created_at: now + 1,
      status: 'superseded',
    });

    const feed = getActivityFeed(ALL_PROJECTS_SCOPE);
    expect(feed).toHaveLength(1);
    expect(feed[0].id).toBe('spore-active');
  });

  // ---------------------------------------------------------------------------
  // Limit parameter respected
  // ---------------------------------------------------------------------------

  it('respects the limit parameter', async () => {
    const base = epochNow();

    // Insert 5 sessions with distinct timestamps
    for (let i = 0; i < 5; i++) {
      upsertSession({
        id: `sess-limit-${i}`,
        agent: 'claude-code',
        started_at: base + i,
        created_at: base + i,
      });
    }

    const feed = getActivityFeed(ALL_PROJECTS_SCOPE, 3);
    expect(feed).toHaveLength(3);
  });

  it('default limit is 50', async () => {
    const base = epochNow();

    // Insert 60 sessions
    for (let i = 0; i < 60; i++) {
      upsertSession({
        id: `sess-def-${String(i).padStart(3, '0')}`,
        agent: 'claude-code',
        started_at: base + i,
        created_at: base + i,
      });
    }

    const feed = getActivityFeed(ALL_PROJECTS_SCOPE);
    expect(feed).toHaveLength(50);
  });

  // ---------------------------------------------------------------------------
  // Agent run uses completed_at as timestamp when available
  // ---------------------------------------------------------------------------

  it('uses completed_at as timestamp for finished agent runs', async () => {
    const now = epochNow();

    insertRun({
      id: 'run-done',
      agent_id: TEST_AGENT_ID,
      task: 'prune',
      status: 'completed',
      started_at: now,
      completed_at: now + 100,
    });

    const feed = getActivityFeed(ALL_PROJECTS_SCOPE);
    expect(feed).toHaveLength(1);
    expect(feed[0].timestamp).toBe(now + 100);
  });

  // ---------------------------------------------------------------------------
  // Only data from requested sources returned
  // ---------------------------------------------------------------------------

  it('returns entries only from tables with data', async () => {
    const now = epochNow();

    // Only a spore — no sessions or runs
    insertSpore({
      id: 'spore-only',
      agent_id: TEST_AGENT_ID,
      observation_type: 'discovery',
      content: 'Interesting finding',
      created_at: now,
    });

    const feed = getActivityFeed(ALL_PROJECTS_SCOPE);
    expect(feed).toHaveLength(1);
    expect(feed[0].type).toBe('spore');
  });

  // ---------------------------------------------------------------------------
  // Multi-tenancy — project scope MUST filter rows by project_id
  // ---------------------------------------------------------------------------

  it('returns only rows belonging to the scoped project (no cross-project leak)', async () => {
    const now = epochNow();
    const projectA = assertGroveProjectId('proj_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
    const projectB = assertGroveProjectId('proj_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');

    upsertSession({
      id: 'sess-A', agent: 'claude-code', started_at: now + 30, created_at: now + 30, title: 'A session',
      project_id: projectA,
    });
    upsertSession({
      id: 'sess-B', agent: 'claude-code', started_at: now + 31, created_at: now + 31, title: 'B session',
      project_id: projectB,
    });

    insertRun({
      id: 'run-A', agent_id: TEST_AGENT_ID, task: 'a-task', status: 'completed',
      started_at: now + 20, completed_at: now + 20, project_id: projectA,
    });
    insertRun({
      id: 'run-B', agent_id: TEST_AGENT_ID, task: 'b-task', status: 'completed',
      started_at: now + 21, completed_at: now + 21, project_id: projectB,
    });

    insertSpore({
      id: 'spore-A', agent_id: TEST_AGENT_ID, observation_type: 'gotcha',
      content: 'A finding', created_at: now + 10, project_id: projectA,
    });
    insertSpore({
      id: 'spore-B', agent_id: TEST_AGENT_ID, observation_type: 'gotcha',
      content: 'B finding', created_at: now + 11, project_id: projectB,
    });

    const feedA = getActivityFeed(projectScope(projectA));
    expect(feedA).toHaveLength(3);
    for (const entry of feedA) {
      expect(entry.id.endsWith('-A')).toBe(true);
    }

    const feedB = getActivityFeed(projectScope(projectB));
    expect(feedB).toHaveLength(3);
    for (const entry of feedB) {
      expect(entry.id.endsWith('-B')).toBe(true);
    }

    // ALL_PROJECTS_SCOPE deliberately leaks for admin views — confirm it
    // sees both sets so consumers can rely on the explicit-opt-in semantics.
    const feedAll = getActivityFeed(ALL_PROJECTS_SCOPE);
    expect(feedAll).toHaveLength(6);
  });
});
