import { describe, expect, it } from 'vitest';
import { AttachProductImage, ListProductImages } from '../application/image-use-cases.js';
import { ProductImageVariantMismatchError } from '../domain/errors.js';
import type { NewProductImage, ProductImage, ProductImageOrderItem } from '../domain/image.js';
import type { NewProduct, Product, ProductPatch } from '../domain/product.js';
import type { ProductImageRepository, ProductRepository } 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,
  };
}

function image(
  input: Partial<ProductImage> & Pick<ProductImage, 'id' | 'productId' | 'url'>,
): ProductImage {
  return {
    variantId: null,
    altText: 'Alt text',
    position: 0,
    role: 'gallery',
    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[]) {}

  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,
    });
    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 true;
  }

  async brandExists(_brandId: string): Promise<boolean> {
    return true;
  }

  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 FakeImageRepository implements ProductImageRepository {
  constructor(
    private readonly images: ProductImage[],
    private readonly validVariants: readonly string[] = [],
  ) {}

  async listByProductId(
    productId: string,
    variantId: string | null = null,
  ): Promise<ProductImage[]> {
    return this.images
      .filter((item) => item.productId === productId && item.variantId === variantId)
      .sort((a, b) => a.position - b.position || a.id.localeCompare(b.id));
  }

  async listByProductIds(productIds: readonly string[]): Promise<ProductImage[]> {
    const set = new Set(productIds);
    return this.images
      .filter((item) => set.has(item.productId))
      .sort((a, b) => a.position - b.position || a.id.localeCompare(b.id));
  }

  async attach(productId: string, input: NewProductImage): Promise<ProductImage> {
    const created = image({
      id: `img-${this.images.length + 1}`,
      productId,
      variantId: input.variantId ?? null,
      url: input.url,
      altText: input.altText,
      position: input.position ?? 0,
      role: input.role,
    });
    this.images.push(created);
    return created;
  }

  async detach(productId: string, imageId: string): Promise<boolean> {
    const index = this.images.findIndex(
      (item) => item.productId === productId && item.id === imageId,
    );
    if (index < 0) return false;
    this.images.splice(index, 1);
    return true;
  }

  async reorder(
    productId: string,
    items: readonly ProductImageOrderItem[],
  ): Promise<ProductImage[]> {
    for (const item of items) {
      const current = this.images.find(
        (imageItem) => imageItem.productId === productId && imageItem.id === item.imageId,
      );
      if (current) current.position = item.position;
    }
    return this.listByProductId(productId);
  }

  async variantBelongsToProduct(_productId: string, variantId: string): Promise<boolean> {
    return this.validVariants.includes(variantId);
  }
}

describe('product image use cases', () => {
  it('lists product images in stable order with alt text', async () => {
    const images = new FakeImageRepository([
      image({ id: 'b', productId: 'prod', url: '/b.jpg', altText: 'Second', position: 2 }),
      image({ id: 'a', productId: 'prod', url: '/a.jpg', altText: 'First', position: 1 }),
    ]);

    const result = await new ListProductImages(images).execute('prod');

    expect(result.map((item) => ({ url: item.url, altText: item.altText }))).toEqual([
      { url: '/a.jpg', altText: 'First' },
      { url: '/b.jpg', altText: 'Second' },
    ]);
  });

  it('returns undefined when attaching an image to a missing product', async () => {
    const products = new FakeProductRepository([]);
    const images = new FakeImageRepository([]);

    const result = await new AttachProductImage(products, images).execute('missing', {
      url: '/image.jpg',
      altText: 'Image',
      role: 'main',
    });

    expect(result).toBeUndefined();
  });

  it('rejects a variant image when the variant does not belong to the product', async () => {
    const products = new FakeProductRepository([
      product({ id: 'prod', name: 'Aceite', slug: 'aceite' }),
    ]);
    const images = new FakeImageRepository([], ['variant-owned-by-prod']);

    await expect(
      new AttachProductImage(products, images).execute('prod', {
        url: '/variant.jpg',
        altText: 'Variant',
        role: 'gallery',
        variantId: 'other-variant',
      }),
    ).rejects.toBeInstanceOf(ProductImageVariantMismatchError);
  });
});
