import { describe, expect, it } from 'vitest';
import {
  CreateCategory,
  ListCategoryTree,
  UpdateCategory,
} from '../application/category-use-cases.js';
import type { Category, CategoryPatch, NewCategory } from '../domain/category.js';
import {
  CategoryParentNotContainerError,
  CategoryParentNotFoundError,
  CategoryTreeCycleError,
} from '../domain/errors.js';
import type { CategoryRepository } from '../domain/ports.js';

function category(input: Partial<Category> & Pick<Category, 'id' | 'name' | 'slug'>): Category {
  return {
    parentId: null,
    seoTitle: null,
    seoDescription: null,
    isParent: false,
    createdAt: new Date('2026-01-01T00:00:00Z'),
    updatedAt: new Date('2026-01-01T00:00:00Z'),
    ...input,
  };
}

class FakeCategoryRepository implements CategoryRepository {
  constructor(private readonly categories: Category[]) {}

  async list(): Promise<Category[]> {
    return this.categories;
  }

  async findById(id: string): Promise<Category | undefined> {
    return this.categories.find((item) => item.id === id);
  }

  async findBySlug(slug: string): Promise<Category | undefined> {
    return this.categories.find((item) => item.slug === slug);
  }

  async create(input: NewCategory): Promise<Category> {
    const created = category({
      id: `cat-${this.categories.length + 1}`,
      parentId: input.parentId ?? null,
      name: input.name,
      slug: input.slug,
      seoTitle: input.seoTitle ?? null,
      seoDescription: input.seoDescription ?? null,
      isParent: input.isParent ?? false,
    });
    this.categories.push(created);
    return created;
  }

  async update(id: string, patch: CategoryPatch): Promise<Category | undefined> {
    const current = await this.findById(id);
    if (!current) {
      return undefined;
    }
    Object.assign(current, patch);
    return current;
  }

  async delete(id: string): Promise<boolean> {
    const index = this.categories.findIndex((item) => item.id === id);
    if (index === -1) {
      return false;
    }
    this.categories.splice(index, 1);
    return true;
  }

  async hasChildren(id: string): Promise<boolean> {
    return this.categories.some((item) => item.parentId === id);
  }

  async isDescendant(candidateAncestorId: string, candidateDescendantId: string): Promise<boolean> {
    let current = await this.findById(candidateDescendantId);
    while (current?.parentId) {
      if (current.parentId === candidateAncestorId) {
        return true;
      }
      current = await this.findById(current.parentId);
    }
    return false;
  }
}

describe('category use cases', () => {
  it('builds a parent/child tree', async () => {
    const repo = new FakeCategoryRepository([
      category({ id: 'root', name: 'Alimentación', slug: 'alimentacion' }),
      category({ id: 'child', parentId: 'root', name: 'Aceites', slug: 'aceites' }),
    ]);

    const tree = await new ListCategoryTree(repo).execute();

    expect(tree).toHaveLength(1);
    expect(tree[0]?.slug).toBe('alimentacion');
    expect(tree[0]?.children[0]?.slug).toBe('aceites');
  });

  it('rejects unknown parent on create', async () => {
    const repo = new FakeCategoryRepository([]);

    await expect(
      new CreateCategory(repo).execute({ parentId: 'missing', name: 'Aceites', slug: 'aceites' }),
    ).rejects.toBeInstanceOf(CategoryParentNotFoundError);
  });

  it('rejects a leaf category as parent', async () => {
    const repo = new FakeCategoryRepository([
      category({ id: 'leaf', name: 'Aceites', slug: 'aceites', isParent: false }),
    ]);

    await expect(
      new CreateCategory(repo).execute({ parentId: 'leaf', name: 'Oliva', slug: 'oliva' }),
    ).rejects.toBeInstanceOf(CategoryParentNotContainerError);
  });

  it('rejects self-parent and descendant-as-parent updates', async () => {
    const repo = new FakeCategoryRepository([
      category({ id: 'root', name: 'Alimentación', slug: 'alimentacion' }),
      category({ id: 'child', parentId: 'root', name: 'Aceites', slug: 'aceites' }),
      category({ id: 'grandchild', parentId: 'child', name: 'Oliva', slug: 'oliva' }),
    ]);
    const update = new UpdateCategory(repo);

    await expect(update.execute('root', { parentId: 'root' })).rejects.toBeInstanceOf(
      CategoryTreeCycleError,
    );
    await expect(update.execute('root', { parentId: 'grandchild' })).rejects.toBeInstanceOf(
      CategoryTreeCycleError,
    );
  });
});
