import { describe, expect, it } from 'vitest';
import { CreateProduct, SearchProducts, UpdateProduct } from '../application/product-use-cases.js';
import { ProductCategoryNotFoundError } from '../domain/errors.js';
import type {
  NewProduct,
  Product,
  ProductPatch,
  ProductSearchCriteria,
} from '../domain/product.js';
import type { ProductRepository, ProductSearchRepository } from '../domain/ports.js';

function product(input: Partial<Product> & Pick<Product, 'id' | 'name' | 'slug'>): Product {
  return {
    description: null,
    state: 'draft',
    channels: 'all',
    featured: false,
    attributes: [],
    seoTitle: null,
    seoDescription: null,
    categoryIds: [],
    brandId: null,
    expirationDate: null,
    unitWeightKg: 1,
    minPurchaseQty: 1,
    aiAssisted: false,
    createdAt: new Date('2026-01-01T00:00:00Z'),
    updatedAt: new Date('2026-01-01T00:00:00Z'),
    ...input,
  };
}

class FakeProductRepository implements ProductRepository {
  constructor(
    private readonly products: Product[],
    private readonly knownCategoryIds: readonly string[] = [],
  ) {}

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

  async findActiveBySlug(slug: string): Promise<Product | undefined> {
    return this.products.find((item) => item.slug === slug && item.state === 'active');
  }

  async create(input: NewProduct): Promise<Product> {
    const created = product({
      id: `prod-${this.products.length + 1}`,
      name: input.name,
      slug: input.slug,
      description: input.description ?? null,
      state: input.state ?? 'draft',
      seoTitle: input.seoTitle ?? null,
      seoDescription: input.seoDescription ?? null,
      categoryIds: input.categoryIds ?? [],
      brandId: input.brandId ?? null,
    });
    this.products.push(created);
    return created;
  }

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

  async categoriesExist(categoryIds: readonly string[]): Promise<boolean> {
    return categoryIds.every((id) => this.knownCategoryIds.includes(id));
  }

  async brandExists(brandId: string): Promise<boolean> {
    return brandId === 'known-brand';
  }

  async delete(_id: string): Promise<void> {
    // noop for tests
  }

  async listAll(options?: {
    limit?: number;
    offset?: number;
    q?: string;
  }): Promise<{ items: Product[]; total: number }> {
    const limit = options?.limit ?? 20;
    const offset = options?.offset ?? 0;
    let items = [...this.products];
    if (options?.q)
      items = items.filter((p) => p.name.toLowerCase().includes(options.q!.toLowerCase()));
    return { items: items.slice(offset, offset + limit), total: items.length };
  }
}

class FakeProductSearchRepository implements ProductSearchRepository {
  public lastInput: ProductSearchCriteria | undefined;

  constructor(private readonly products: Product[]) {}

  async search(input: ProductSearchCriteria): Promise<Product[]> {
    this.lastInput = input;
    return this.products.filter((item) => !input.activeOnly || item.state === 'active');
  }

  async suggestCorrections(_term: string): Promise<string[]> {
    return [];
  }
}

describe('product use cases', () => {
  it('search returns only active products', async () => {
    const repo = new FakeProductSearchRepository([
      product({ id: 'draft', name: 'Draft', slug: 'draft', state: 'draft' }),
      product({ id: 'active', name: 'Active', slug: 'active', state: 'active' }),
      product({ id: 'archived', name: 'Archived', slug: 'archived', state: 'archived' }),
    ]);

    const result = await new SearchProducts(repo).execute({
      q: 'active',
      limit: 10,
      offset: 5,
      brandSlug: 'marca',
      categorySlug: 'categoria',
    });

    expect(result.map((item) => item.slug)).toEqual(['active']);
    expect(repo.lastInput).toEqual({
      q: 'active',
      limit: 10,
      offset: 5,
      activeOnly: true,
      brandSlug: 'marca',
      categorySlug: 'categoria',
    });
  });

  it('create defaults product state to draft', async () => {
    const repo = new FakeProductRepository([]);

    const created = await new CreateProduct(repo).execute({ name: 'Aceite', slug: 'aceite' });

    expect(created.state).toBe('draft');
  });

  it('create and update reject unknown category ids', async () => {
    const repo = new FakeProductRepository(
      [product({ id: 'prod', name: 'Aceite', slug: 'aceite' })],
      ['known-category'],
    );

    await expect(
      new CreateProduct(repo).execute({ name: 'Pan', slug: 'pan', categoryIds: ['missing'] }),
    ).rejects.toBeInstanceOf(ProductCategoryNotFoundError);
    await expect(
      new UpdateProduct(repo).execute('prod', { categoryIds: ['missing'] }),
    ).rejects.toBeInstanceOf(ProductCategoryNotFoundError);
  });
});
