/**
 * DrizzleErrandRepository Tests
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { DrizzleErrandRepository } from './errand.js';
import { createTestDatabase } from './test-helpers.js';
import type { DrizzleDatabase } from '../../index.js';
import { projects, agents, errands } from '../../schema.js';
import { nanoid } from 'nanoid';
import { eq } from 'drizzle-orm';

describe('DrizzleErrandRepository', () => {
  let db: DrizzleDatabase;
  let repo: DrizzleErrandRepository;

  beforeEach(() => {
    db = createTestDatabase();
    repo = new DrizzleErrandRepository(db);
  });

  // Helper: create a project record
  async function createProject(name = 'Test Project', suffix = '') {
    const id = nanoid();
    const now = new Date();
    const [project] = await db.insert(projects).values({
      id,
      name: name + suffix + id,
      url: `https://github.com/test/${id}`,
      defaultBranch: 'main',
      createdAt: now,
      updatedAt: now,
    }).returning();
    return project;
  }

  // Helper: create an agent record
  async function createAgent(name?: string) {
    const id = nanoid();
    const now = new Date();
    const agentName = name ?? `agent-${id}`;
    const [agent] = await db.insert(agents).values({
      id,
      name: agentName,
      worktreeId: `agent-workdirs/${agentName}`,
      provider: 'claude',
      status: 'idle',
      mode: 'execute',
      createdAt: now,
      updatedAt: now,
    }).returning();
    return agent;
  }

  // Helper: create an errand
  async function createErrand(overrides: Partial<{
    id: string;
    description: string;
    branch: string;
    baseBranch: string;
    agentId: string | null;
    projectId: string | null;
    status: 'active' | 'pending_review' | 'conflict' | 'merged' | 'abandoned';
    createdAt: Date;
  }> = {}) {
    const project = await createProject();
    const id = overrides.id ?? nanoid();
    return repo.create({
      id,
      description: overrides.description ?? 'Test errand',
      branch: overrides.branch ?? 'feature/test',
      baseBranch: overrides.baseBranch ?? 'main',
      agentId: overrides.agentId !== undefined ? overrides.agentId : null,
      projectId: overrides.projectId !== undefined ? overrides.projectId : project.id,
      status: overrides.status ?? 'active',
    });
  }

  describe('create + findById', () => {
    it('should create errand and find by id with all fields', async () => {
      const project = await createProject();
      const id = nanoid();

      await repo.create({
        id,
        description: 'Fix the bug',
        branch: 'fix/bug-123',
        baseBranch: 'main',
        agentId: null,
        projectId: project.id,
        status: 'active',
      });

      const found = await repo.findById(id);
      expect(found).toBeDefined();
      expect(found!.id).toBe(id);
      expect(found!.description).toBe('Fix the bug');
      expect(found!.branch).toBe('fix/bug-123');
      expect(found!.baseBranch).toBe('main');
      expect(found!.status).toBe('active');
      expect(found!.projectId).toBe(project.id);
      expect(found!.agentId).toBeNull();
      expect(found!.agentAlias).toBeNull();
    });
  });

  describe('findAll', () => {
    it('should return all errands ordered by createdAt desc', async () => {
      const project = await createProject();
      const t1 = new Date('2024-01-01T00:00:00Z');
      const t2 = new Date('2024-01-02T00:00:00Z');
      const t3 = new Date('2024-01-03T00:00:00Z');

      const id1 = nanoid();
      const id2 = nanoid();
      const id3 = nanoid();

      await db.insert(errands).values([
        { id: id1, description: 'Errand 1', branch: 'b1', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t1, updatedAt: t1 },
        { id: id2, description: 'Errand 2', branch: 'b2', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t2, updatedAt: t2 },
        { id: id3, description: 'Errand 3', branch: 'b3', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t3, updatedAt: t3 },
      ]);

      const result = await repo.findAll();
      expect(result.length).toBeGreaterThanOrEqual(3);
      // Find our three in the results
      const ids = result.map((e) => e.id);
      expect(ids.indexOf(id3)).toBeLessThan(ids.indexOf(id2));
      expect(ids.indexOf(id2)).toBeLessThan(ids.indexOf(id1));
    });

    it('should filter by projectId', async () => {
      const projectA = await createProject('A');
      const projectB = await createProject('B');
      const now = new Date();

      const idA1 = nanoid();
      const idA2 = nanoid();
      const idB1 = nanoid();

      await db.insert(errands).values([
        { id: idA1, description: 'A1', branch: 'b-a1', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
        { id: idA2, description: 'A2', branch: 'b-a2', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
        { id: idB1, description: 'B1', branch: 'b-b1', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'active', createdAt: now, updatedAt: now },
      ]);

      const result = await repo.findAll({ projectId: projectA.id });
      expect(result).toHaveLength(2);
      expect(result.map((e) => e.id).sort()).toEqual([idA1, idA2].sort());
    });

    it('should filter by status', async () => {
      const project = await createProject();
      const now = new Date();

      const id1 = nanoid();
      const id2 = nanoid();
      const id3 = nanoid();

      await db.insert(errands).values([
        { id: id1, description: 'E1', branch: 'b1', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: now, updatedAt: now },
        { id: id2, description: 'E2', branch: 'b2', baseBranch: 'main', agentId: null, projectId: project.id, status: 'pending_review', createdAt: now, updatedAt: now },
        { id: id3, description: 'E3', branch: 'b3', baseBranch: 'main', agentId: null, projectId: project.id, status: 'merged', createdAt: now, updatedAt: now },
      ]);

      const result = await repo.findAll({ status: 'pending_review' });
      expect(result).toHaveLength(1);
      expect(result[0].id).toBe(id2);
    });

    it('should filter by both projectId and status', async () => {
      const projectA = await createProject('PA');
      const projectB = await createProject('PB');
      const now = new Date();

      const idMatch = nanoid();
      const idOtherStatus = nanoid();
      const idOtherProject = nanoid();
      const idNeither = nanoid();

      await db.insert(errands).values([
        { id: idMatch, description: 'Match', branch: 'b1', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'pending_review', createdAt: now, updatedAt: now },
        { id: idOtherStatus, description: 'Wrong status', branch: 'b2', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
        { id: idOtherProject, description: 'Wrong project', branch: 'b3', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'pending_review', createdAt: now, updatedAt: now },
        { id: idNeither, description: 'Neither', branch: 'b4', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'active', createdAt: now, updatedAt: now },
      ]);

      const result = await repo.findAll({ projectId: projectA.id, status: 'pending_review' });
      expect(result).toHaveLength(1);
      expect(result[0].id).toBe(idMatch);
    });
  });

  describe('findById', () => {
    it('should return agentAlias when agentId is set', async () => {
      const agent = await createAgent('known-agent');
      const project = await createProject();
      const id = nanoid();
      const now = new Date();

      await db.insert(errands).values({
        id,
        description: 'With agent',
        branch: 'feature/x',
        baseBranch: 'main',
        agentId: agent.id,
        projectId: project.id,
        status: 'active',
        createdAt: now,
        updatedAt: now,
      });

      const found = await repo.findById(id);
      expect(found).toBeDefined();
      expect(found!.agentAlias).toBe(agent.name);
    });

    it('should return agentAlias as null when agentId is null', async () => {
      const project = await createProject();
      const id = nanoid();
      const now = new Date();

      await db.insert(errands).values({
        id,
        description: 'No agent',
        branch: 'feature/y',
        baseBranch: 'main',
        agentId: null,
        projectId: project.id,
        status: 'active',
        createdAt: now,
        updatedAt: now,
      });

      const found = await repo.findById(id);
      expect(found).toBeDefined();
      expect(found!.agentAlias).toBeNull();
    });

    it('should return undefined for unknown id', async () => {
      const found = await repo.findById('nonexistent');
      expect(found).toBeUndefined();
    });
  });

  describe('update', () => {
    it('should update status and advance updatedAt', async () => {
      const project = await createProject();
      const id = nanoid();
      const past = new Date('2024-01-01T00:00:00Z');

      await db.insert(errands).values({
        id,
        description: 'Errand',
        branch: 'feature/update',
        baseBranch: 'main',
        agentId: null,
        projectId: project.id,
        status: 'active',
        createdAt: past,
        updatedAt: past,
      });

      const updated = await repo.update(id, { status: 'pending_review' });
      expect(updated.status).toBe('pending_review');
      expect(updated.updatedAt.getTime()).toBeGreaterThan(past.getTime());
    });

    it('should throw on unknown id', async () => {
      await expect(
        repo.update('nonexistent', { status: 'merged' })
      ).rejects.toThrow('Errand not found');
    });
  });

  describe('delete', () => {
    it('should delete errand and findById returns undefined', async () => {
      const errand = await createErrand();
      await repo.delete(errand.id);
      const found = await repo.findById(errand.id);
      expect(found).toBeUndefined();
    });
  });

  describe('cascade and set null', () => {
    it('should cascade delete errands when project is deleted', async () => {
      const project = await createProject();
      const id = nanoid();
      const now = new Date();

      await db.insert(errands).values({
        id,
        description: 'Cascade test',
        branch: 'feature/cascade',
        baseBranch: 'main',
        agentId: null,
        projectId: project.id,
        status: 'active',
        createdAt: now,
        updatedAt: now,
      });

      // Delete project — should cascade delete errands
      await db.delete(projects).where(eq(projects.id, project.id));

      const found = await repo.findById(id);
      expect(found).toBeUndefined();
    });

    it('should set agentId to null when agent is deleted', async () => {
      const agent = await createAgent();
      const project = await createProject();
      const id = nanoid();
      const now = new Date();

      await db.insert(errands).values({
        id,
        description: 'Agent null test',
        branch: 'feature/agent-null',
        baseBranch: 'main',
        agentId: agent.id,
        projectId: project.id,
        status: 'active',
        createdAt: now,
        updatedAt: now,
      });

      // Delete agent — should set null
      await db.delete(agents).where(eq(agents.id, agent.id));

      const [errand] = await db.select().from(errands).where(eq(errands.id, id));
      expect(errand).toBeDefined();
      expect(errand.agentId).toBeNull();
    });
  });
});
