import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue'
import { useProductStore, type ProductRecord } from '@/stores/product'

// jsdom 不支持 matchMedia，Ant Design Vue 响应式布局需要
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: (query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: () => {},
    removeListener: () => {},
    addEventListener: () => {},
    removeEventListener: () => {},
    dispatchEvent: () => false,
  }),
})

vi.mock('@/utils/request', () => ({
  api: {
    get: vi.fn(),
    post: vi.fn(),
    put: vi.fn(),
    patch: vi.fn(),
  },
}))

const mockProducts: ProductRecord[] = [
  {
    id: 1,
    company_id: 1,
    platform_id: 1,
    store_id: 1,
    status_id: 1,
    platform_item_id: 'TB001',
    platform_model_id: 'SKU001',
    name: '测试商品A',
    price: '99.00',
    currency: 'CNY',
    num: 100,
    created_date: '2026-01-01T00:00:00Z',
    updated_date: '2026-03-01T10:30:00Z',
  },
  {
    id: 2,
    company_id: 2,
    platform_id: 1,
    store_id: 2,
    status_id: 3,
    platform_item_id: 'TB002',
    platform_model_id: null,
    name: '测试商品B - 这是一个非常长的商品名称用于测试省略号效果',
    price: '199.50',
    currency: 'USD',
    num: 0,
    created_date: '2026-02-01T00:00:00Z',
    updated_date: '2026-03-15T08:00:00Z',
  },
]

const mockPaginatedResponse = {
  items: mockProducts,
  total: 2,
  page: 1,
  per_page: 15,
}

const mockProductDetail = {
  ...mockProducts[0],
  type_id: 1,
  warehouse_id: null,
  sub_warehouse_id: null,
  origin_sku_id: 'ORIG001',
  mapped_sku_id: 'MAP001',
  barcode: '1234567890',
  hscode: '6403',
  bundled: null,
  url: 'https://example.com/product/1',
  picture: 'https://example.com/img/1.jpg',
  ext: { custom_field: 'value', nested: { a: 1 } },
  created_at: '2026-01-01T00:00:00Z',
  updated_at: '2026-03-01T10:30:00Z',
}

const mockRawDetail = {
  id: 1,
  platform_item_id: 'TB001',
  platform_model_id: 'SKU001',
  name: '测试商品A',
  store_id: 1,
  company_id: 1,
  platform_id: 1,
  raw: { item_id: 'tb_001', title: 'Raw Title', attrs: [1, 2, 3] },
  hash: 'abc123',
  ext: null,
  created_date: '2026-01-01T00:00:00Z',
  updated_date: '2026-03-01T10:30:00Z',
}

const mockLookupCompanies = [{ id: 1, name: 'Company A', label: '公司A' }, { id: 2, name: 'Company B', label: '公司B' }]
const mockLookupPlatforms = [{ id: 1, developer_id: 1 }]
const mockLookupStores = [
  { id: 1, company_id: 1, platform_id: 1, name: 'Store 1', label: '店铺1' },
  { id: 2, company_id: 2, platform_id: 1, name: 'Store 2', label: '店铺2' },
]

// ─── Store Tests ───────────────────────────────────────

describe('useProductStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.restoreAllMocks()
  })

  describe('initial state', () => {
    it('starts with empty products and default pagination', () => {
      const store = useProductStore()
      expect(store.products).toEqual([])
      expect(store.loading).toBe(false)
      expect(store.pagination.page).toBe(1)
      expect(store.pagination.per_page).toBe(15)
      expect(store.pagination.total).toBe(0)
      expect(store.filters.name).toBe('')
      expect(store.filters.platform_item_id).toBe('')
      expect(store.filters.status_id).toBeUndefined()
    })
  })

  describe('fetchProducts', () => {
    it('loads data and updates state', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)

      const store = useProductStore()
      await store.fetchProducts()

      expect(api.get).toHaveBeenCalledWith('/api/v1/products', expect.objectContaining({
        page: 1,
        per_page: 15,
        name: undefined,
        platform_item_id: undefined,
        status_id: undefined,
      }))
      expect(store.products).toEqual(mockProducts)
      expect(store.pagination.total).toBe(2)
    })

    it('passes filter params correctly', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)

      const store = useProductStore()
      store.filters.name = '测试'
      store.filters.platform_item_id = 'TB001'
      store.filters.status_id = 1
      store.cascadeValue.company_id = 1
      store.cascadeValue.platform_id = 2
      store.cascadeValue.store_id = 3

      await store.fetchProducts()

      expect(api.get).toHaveBeenCalledWith('/api/v1/products', expect.objectContaining({
        name: '测试',
        platform_item_id: 'TB001',
        status_id: 1,
        company_id: 1,
        platform_id: 2,
        store_id: 3,
      }))
    })

    it('shows error message on failure', async () => {
      const { api } = await import('@/utils/request')
      const { message } = await import('ant-design-vue')
      const errorSpy = vi.spyOn(message, 'error')
      vi.mocked(api.get).mockRejectedValueOnce(new Error('Network error'))

      const store = useProductStore()
      await store.fetchProducts()

      expect(errorSpy).toHaveBeenCalledWith('Network error')
      expect(store.products).toEqual([])
    })
  })

  describe('resetFilters', () => {
    it('clears all filters and resets page to 1', () => {
      const store = useProductStore()
      store.filters.name = '测试'
      store.filters.platform_item_id = 'TB001'
      store.filters.status_id = 1
      store.cascadeValue.company_id = 1
      store.cascadeValue.platform_id = 2
      store.cascadeValue.store_id = 3
      store.pagination.page = 5

      store.resetFilters()

      expect(store.filters.name).toBe('')
      expect(store.filters.platform_item_id).toBe('')
      expect(store.filters.status_id).toBeUndefined()
      expect(store.cascadeValue.company_id).toBeUndefined()
      expect(store.cascadeValue.platform_id).toBeUndefined()
      expect(store.cascadeValue.store_id).toBeUndefined()
      expect(store.pagination.page).toBe(1)
    })
  })

  describe('name maps', () => {
    it('builds lookup maps from loaded data', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get)
        .mockResolvedValueOnce(mockLookupCompanies)
        .mockResolvedValueOnce(mockLookupPlatforms)
        .mockResolvedValueOnce(mockLookupStores)

      const store = useProductStore()
      await store.loadLookups()

      expect(store.companyMap.get(1)).toBe('公司A')
      expect(store.platformMap.get(1)).toBe('平台 #1')
      expect(store.storeMap.get(1)).toBe('店铺1')
    })

    it('handles loadLookups failure gracefully', async () => {
      const { api } = await import('@/utils/request')
      const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
      vi.mocked(api.get).mockRejectedValueOnce(new Error('Network error'))

      const store = useProductStore()
      await store.loadLookups()

      expect(warnSpy).toHaveBeenCalledWith('加载查找表数据失败', expect.any(Error))
      expect(store.companyMap.size).toBe(0)
      expect(store.platformMap.size).toBe(0)
      expect(store.storeMap.size).toBe(0)
      warnSpy.mockRestore()
    })
  })

  describe('fetchProducts error handling', () => {
    it('clears products on fetch failure', async () => {
      const { api } = await import('@/utils/request')
      const { message } = await import('ant-design-vue')
      vi.spyOn(message, 'error')

      const store = useProductStore()
      // 先加载一些数据
      vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse)
      await store.fetchProducts()
      expect(store.products.length).toBe(2)

      // 再让请求失败
      vi.mocked(api.get).mockRejectedValueOnce(new Error('Server error'))
      await store.fetchProducts()

      expect(store.products).toEqual([])
      expect(store.pagination.total).toBe(0)
    })
  })
})

// ─── Page Component Tests ──────────────────────────────

describe('ProductsPage', () => {
  let wrapper: ReturnType<typeof mount>

  beforeEach(() => {
    setActivePinia(createPinia())
    vi.restoreAllMocks()
    document.body.innerHTML = ''
  })

  afterEach(() => {
    wrapper?.unmount()
    document.body.innerHTML = ''
  })

  async function mountPage() {
    const { api } = await import('@/utils/request')
    vi.mocked(api.get).mockImplementation((url: string) => {
      if (url === '/api/v1/products') return Promise.resolve(mockPaginatedResponse) as never
      if (url === '/api/v1/companies') return Promise.resolve(mockLookupCompanies) as never
      if (url === '/api/v1/platforms') return Promise.resolve(mockLookupPlatforms) as never
      if (url === '/api/v1/stores') return Promise.resolve(mockLookupStores) as never
      return Promise.resolve([]) as never
    })

    const { default: ProductsPage } = await import('../index.vue')

    wrapper = mount(ProductsPage, {
      attachTo: document.body,
      global: {
        stubs: {
          SearchOutlined: { template: '<span />' },
          ReloadOutlined: { template: '<span />' },
          EyeOutlined: { template: '<span />' },
          CascadeFilter: { template: '<div class="cascade-filter-stub" />' },
        },
      },
    })
    await flushPromises()
    await nextTick()
    return { wrapper, api }
  }

  it('renders page title and action buttons', async () => {
    await mountPage()

    expect(wrapper.text()).toContain('产品管理')
    const buttons = wrapper.findAll('.ant-btn')
    const buttonTexts = buttons.map((b) => b.text())
    expect(buttonTexts.some((t) => t.includes('搜索'))).toBe(true)
    expect(buttonTexts.some((t) => t.includes('重置'))).toBe(true)
  }, 15000)

  it('calls fetchProducts on mount', async () => {
    const { api } = await mountPage()

    expect(api.get).toHaveBeenCalledWith('/api/v1/products', expect.objectContaining({
      page: 1,
      per_page: 15,
    }))
  })

  it('renders table with product data', async () => {
    await mountPage()

    const html = wrapper.html()
    expect(html).toContain('测试商品A')
    expect(html).toContain('¥99.00')
    expect(html).toContain('$199.50')
  })

  it('renders status tags with correct text', async () => {
    await mountPage()

    const tags = wrapper.findAll('.ant-tag')
    const tagTexts = tags.map((t) => t.text())
    // mockProducts: status_id=1 → 草稿, status_id=3 → 下架
    expect(tagTexts).toContain('草稿')
    expect(tagTexts).toContain('下架')
  })

  it('renders company/platform/store names from lookup maps', async () => {
    await mountPage()

    const html = wrapper.html()
    expect(html).toContain('公司A')
    expect(html).toContain('店铺1')
  })

  it('renders CascadeFilter component', async () => {
    await mountPage()

    expect(wrapper.find('.cascade-filter-stub').exists()).toBe(true)
  })

  it('search button resets page to 1 and fetches', async () => {
    const { api } = await mountPage()

    const store = useProductStore()
    store.pagination.page = 3
    store.filters.name = '测试'
    vi.mocked(api.get).mockClear()
    vi.mocked(api.get).mockResolvedValue(mockPaginatedResponse)

    const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
    const searchBtn = buttons.find((b) => b.textContent?.trim().includes('搜索'))
    searchBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
    await flushPromises()
    await nextTick()

    expect(store.pagination.page).toBe(1)
    expect(api.get).toHaveBeenCalledWith('/api/v1/products', expect.objectContaining({
      page: 1,
      name: '测试',
    }))
  })

  it('resets filters when clicking reset button', async () => {
    await mountPage()

    const store = useProductStore()
    store.filters.name = '测试'
    store.filters.status_id = 1

    const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
    const resetBtn = buttons.find((b) => b.textContent?.trim().includes('重置'))
    expect(resetBtn).toBeDefined()

    resetBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
    await flushPromises()
    await nextTick()

    expect(store.filters.name).toBe('')
    expect(store.filters.status_id).toBeUndefined()
  })

  it('opens detail drawer when clicking view button', async () => {
    const { api } = await mountPage()
    vi.mocked(api.get)
      .mockResolvedValueOnce(mockProductDetail)
      .mockResolvedValueOnce(mockRawDetail)

    const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
    const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
    expect(viewBtn).toBeDefined()

    viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
    await flushPromises()
    await nextTick()

    const drawer = document.body.querySelector('.ant-drawer')
    expect(drawer).toBeTruthy()
  })

  it('displays ext JSON in drawer when available', async () => {
    const { api } = await mountPage()
    vi.mocked(api.get)
      .mockResolvedValueOnce(mockProductDetail)
      .mockResolvedValueOnce(mockRawDetail)

    const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
    const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
    viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
    await flushPromises()
    await nextTick()

    const drawerHtml = document.body.querySelector('.ant-drawer')?.innerHTML || ''
    expect(drawerHtml).toContain('custom_field')
    expect(drawerHtml).toContain('"value"')
  })

  it('displays raw JSON in drawer when available', async () => {
    const { api } = await mountPage()
    vi.mocked(api.get)
      .mockResolvedValueOnce(mockProductDetail)
      .mockResolvedValueOnce(mockRawDetail)

    const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
    const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
    viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
    await flushPromises()
    await nextTick()

    const drawerHtml = document.body.querySelector('.ant-drawer')?.innerHTML || ''
    expect(drawerHtml).toContain('item_id')
    expect(drawerHtml).toContain('tb_001')
  })

  describe('status mapping coverage', () => {
    const statusExpectations: Array<{ id: number; text: string; color: string }> = [
      { id: 1, text: '草稿', color: 'default' },
      { id: 2, text: '在售', color: 'green' },
      { id: 3, text: '下架', color: 'orange' },
      { id: 4, text: '封禁', color: 'red' },
      { id: 5, text: '审核中', color: 'blue' },
      { id: 6, text: '卖家删除', color: 'volcano' },
      { id: 7, text: '平台删除', color: 'volcano' },
    ]

    it.each(statusExpectations)(
      'renders status_id=$id as "$text"',
      async ({ id, text }) => {
        const { api } = await import('@/utils/request')
        const singleProduct = { ...mockProducts[0], id: 100 + id, status_id: id }
        vi.mocked(api.get).mockImplementation((url: string) => {
          if (url === '/api/v1/products')
            return Promise.resolve({ items: [singleProduct], total: 1, page: 1, per_page: 15 }) as never
          if (url === '/api/v1/companies') return Promise.resolve(mockLookupCompanies) as never
          if (url === '/api/v1/platforms') return Promise.resolve(mockLookupPlatforms) as never
          if (url === '/api/v1/stores') return Promise.resolve(mockLookupStores) as never
          return Promise.resolve([]) as never
        })

        const { default: ProductsPage } = await import('../index.vue')
        wrapper = mount(ProductsPage, {
          attachTo: document.body,
          global: {
            stubs: {
              SearchOutlined: { template: '<span />' },
              ReloadOutlined: { template: '<span />' },
              EyeOutlined: { template: '<span />' },
              CascadeFilter: { template: '<div class="cascade-filter-stub" />' },
            },
          },
        })
        await flushPromises()
        await nextTick()

        const tags = wrapper.findAll('.ant-tag')
        const tagTexts = tags.map((t) => t.text())
        expect(tagTexts).toContain(text)
      },
    )

    it('renders fallback for unknown status_id', async () => {
      const { api } = await import('@/utils/request')
      const unknownProduct = { ...mockProducts[0], id: 999, status_id: 99 }
      vi.mocked(api.get).mockImplementation((url: string) => {
        if (url === '/api/v1/products')
          return Promise.resolve({ items: [unknownProduct], total: 1, page: 1, per_page: 15 }) as never
        if (url === '/api/v1/companies') return Promise.resolve(mockLookupCompanies) as never
        if (url === '/api/v1/platforms') return Promise.resolve(mockLookupPlatforms) as never
        if (url === '/api/v1/stores') return Promise.resolve(mockLookupStores) as never
        return Promise.resolve([]) as never
      })

      const { default: ProductsPage } = await import('../index.vue')
      wrapper = mount(ProductsPage, {
        attachTo: document.body,
        global: {
          stubs: {
            SearchOutlined: { template: '<span />' },
            ReloadOutlined: { template: '<span />' },
            EyeOutlined: { template: '<span />' },
            CascadeFilter: { template: '<div class="cascade-filter-stub" />' },
          },
        },
      })
      await flushPromises()
      await nextTick()

      const tags = wrapper.findAll('.ant-tag')
      const tagTexts = tags.map((t) => t.text())
      expect(tagTexts).toContain('状态 99')
    })
  })

  it('renders all 7 status filter options', async () => {
    await mountPage()

    // Ant Design Vue 渲染 select options 到 body portal，需先打开下拉
    const selectEl = document.body.querySelector('.ant-select-selector')
    selectEl?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
    await flushPromises()
    await nextTick()

    const optionEls = document.body.querySelectorAll('.ant-select-item-option')
    const optionTexts = Array.from(optionEls).map((el) => el.textContent?.trim())
    expect(optionTexts).toContain('草稿')
    expect(optionTexts).toContain('在售')
    expect(optionTexts).toContain('下架')
    expect(optionTexts).toContain('封禁')
    expect(optionTexts).toContain('审核中')
    expect(optionTexts).toContain('卖家删除')
    expect(optionTexts).toContain('平台删除')
    expect(optionTexts.length).toBe(7)
  })

  it('shows placeholder when ext/raw is null', async () => {
    const { api } = await mountPage()
    vi.mocked(api.get)
      .mockResolvedValueOnce({ ...mockProductDetail, ext: null })
      .mockRejectedValueOnce(new Error('Not found'))

    const buttons = Array.from(document.body.querySelectorAll('.ant-btn'))
    const viewBtn = buttons.find((b) => b.textContent?.trim() === '查看')
    viewBtn?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
    await flushPromises()
    await nextTick()

    const drawerHtml = document.body.querySelector('.ant-drawer')?.innerHTML || ''
    expect(drawerHtml).toContain('暂无数据')
  })
})
