import { afterEach, describe, expect, it, vi } from 'vitest';
import type { ToolContext } from './core.js';
import { executeTool, ensureKeepaGraphs, TOOL_DEFS } from './amazon.js';
import type { AmazonProductData } from './structured-blocks.js';

afterEach(() => {
  vi.unstubAllGlobals();
});

const ctx = (toolsConfig: Record<string, unknown> = {}): ToolContext =>
  ({ toolsConfig }) as unknown as ToolContext;

function productBlock(asin: string, title: string, opts: { price?: string; rating?: string; reviews?: string } = {}): string {
  return `
<div data-asin="${asin}" data-component-type="s-search-result">
  <img class="s-image" src="https://m.media-amazon.com/images/${asin}.jpg" alt=""/>
  <h2 class="title"><a href="/dp/${asin}"><span>${title}</span></a></h2>
  ${opts.price ? `<span class="a-price"><span class="a-offscreen">${opts.price}</span></span>` : ''}
  ${opts.rating ? `<span class="a-icon-alt">5つ星のうち${opts.rating}</span>` : ''}
  ${opts.reviews ? `<span aria-label="${opts.reviews}件の評価">${opts.reviews}</span>` : ''}
</div>`;
}

const SEARCH_HTML = `<html><body>
${productBlock('B012345678', 'ワイヤレスイヤホン &amp; ケース', { price: '¥12,980', rating: '4.5', reviews: '1,234' })}
${productBlock('B087654321', 'モバイルバッテリー')}
</body></html>`;

function htmlResponse(body: string, status = 200): Response {
  return new Response(body, { status, headers: { 'content-type': 'text/html' } });
}

describe('TOOL_DEFS', () => {
  it('registers SearchAmazon with query required', () => {
    expect(TOOL_DEFS['SearchAmazon']?.function.name).toBe('SearchAmazon');
    expect(TOOL_DEFS['SearchAmazon']?.function.parameters?.['required']).toEqual(['query']);
  });
});

describe('SearchAmazon', () => {
  it('returns null for unknown tool names', async () => {
    expect(await executeTool('NotAmazon', {}, ctx())).toBeNull();
  });

  it('requires a query', async () => {
    const res = await executeTool('SearchAmazon', {}, ctx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('query is required');
  });

  it('parses products into markdown with Keepa graphs and structured blocks', async () => {
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse(SEARCH_HTML)));
    const res = await executeTool('SearchAmazon', { query: 'イヤホン' }, ctx());

    expect(res?.isError).toBe(false);
    expect(res?.output).toContain('## Amazon.co.jp 検索結果: 「イヤホン」');
    // Entity-decoded title, price, rating with review count.
    expect(res?.output).toContain('ワイヤレスイヤホン & ケース');
    expect(res?.output).toContain('- **価格**: ¥12,980');
    expect(res?.output).toContain('- **評価**: 4.5 (1,234件)');
    expect(res?.output).toContain('![商品画像](https://m.media-amazon.com/images/B012345678.jpg)');
    expect(res?.output).toContain('graph.keepa.com/pricehistory.png?asin=B012345678&domain=co.jp');
    expect(res?.output).toContain('https://www.amazon.co.jp/dp/B012345678');
    expect(res?.output).toMatch(/\[\[embed:amazon-\d+\]\]/);

    const blocks = res?.structuredBlocks ?? [];
    expect(blocks).toHaveLength(1);
    expect(blocks[0]?.type).toBe('amazon_products');
    const data = blocks[0]?.data as AmazonProductData;
    expect(data.products).toHaveLength(2);
    expect(data.products[0]).toMatchObject({
      asin: 'B012345678',
      rating: 4.5,
      reviewCount: 1234,
    });
  });

  it('parses an integer rating from 5つ星のうち5', async () => {
    const html = `<html><body>${productBlock('B099999999', '満点商品', { rating: '5', reviews: '10' })}</body></html>`;
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse(html)));
    const res = await executeTool('SearchAmazon', { query: 'x' }, ctx());
    expect(res?.output).toContain('- **評価**: 5 (10件)');
  });

  it('appends the affiliate tag from tools config to product URLs', async () => {
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse(SEARCH_HTML)));
    const res = await executeTool(
      'SearchAmazon',
      { query: 'イヤホン' },
      ctx({ amazonAffiliateTag: 'mytag-22' }),
    );
    expect(res?.output).toContain('https://www.amazon.co.jp/dp/B012345678?tag=mytag-22');
  });

  it('caps max_results', async () => {
    const many = `<html><body>${Array.from({ length: 12 }, (_, i) =>
      productBlock(`B0000000${String(i).padStart(2, '0')}`.slice(0, 10), `商品${i}`),
    ).join('\n')}</body></html>`;
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse(many)));
    const res = await executeTool('SearchAmazon', { query: 'x', max_results: 3 }, ctx());
    const data = (res?.structuredBlocks?.[0]?.data ?? { products: [] }) as AmazonProductData;
    expect(data.products).toHaveLength(3);
  });

  it('suggests BrowseWeb when parsing finds nothing (likely blocked)', async () => {
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse('<html>captcha</html>')));
    const res = await executeTool('SearchAmazon', { query: 'イヤホン' }, ctx());
    expect(res?.isError).toBe(false);
    expect(res?.output).toContain('BrowseWeb');
    expect(res?.output).toContain('取得できませんでした');
  });

  it('reports HTTP failures as tool errors with a fallback hint', async () => {
    vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => htmlResponse('nope', 503)));
    const res = await executeTool('SearchAmazon', { query: 'イヤホン' }, ctx());
    expect(res?.isError).toBe(true);
    expect(res?.output).toContain('Amazon 検索に失敗しました');
    expect(res?.output).toContain('BrowseWeb');
  });
});

describe('ensureKeepaGraphs', () => {
  it('appends missing Keepa graphs for ASINs referenced in the text', () => {
    const text = 'おすすめ: https://www.amazon.co.jp/dp/B012345678 です。';
    const out = ensureKeepaGraphs(text);
    expect(out).toContain('### 価格推移 (Keepa)');
    expect(out).toContain('pricehistory.png?asin=B012345678');
  });

  it('leaves text alone when graphs are already present', () => {
    const text = [
      'https://www.amazon.co.jp/dp/B012345678',
      '![価格推移](https://graph.keepa.com/pricehistory.png?asin=B012345678&domain=co.jp)',
    ].join('\n');
    expect(ensureKeepaGraphs(text)).toBe(text);
  });

  it('leaves text without ASINs untouched', () => {
    expect(ensureKeepaGraphs('no products here')).toBe('no products here');
  });

  it('only appends graphs for the ASINs that are missing', () => {
    const text = [
      'https://www.amazon.co.jp/dp/B012345678',
      'https://www.amazon.co.jp/dp/B087654321',
      '![価格推移](https://graph.keepa.com/pricehistory.png?asin=B012345678&domain=co.jp)',
    ].join('\n');
    const out = ensureKeepaGraphs(text);
    expect(out.match(/pricehistory\.png\?asin=B087654321/g)).toHaveLength(1);
    expect(out.match(/pricehistory\.png\?asin=B012345678/g)).toHaveLength(1);
  });
});
