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 { useOperationLogStore } from '@/stores/operation-log'
import type { OperationLogRecord, OperationLogDetail } from '@/types/api'

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 mockLogs: OperationLogRecord[] = [
  {
    id: 1,
    user_id: 1,
    action: 'user.create',
    target_type: 'user',
    target_id: 5,
    description: '创建用户 test_user',
    ip: '127.0.0.1',
    created_at: '2026-03-20T10:30:00Z',
  },
  {
    id: 2,
    user_id: 1,
    action: 'role.update',
    target_type: 'role',
    target_id: 3,
    description: '更新角色权限',
    ip: '192.168.1.1',
    created_at: '2026-03-20T11:00:00Z',
  },
  {
    id: 3,
    user_id: 2,
    action: 'order.delete',
    target_type: 'order',
    target_id: 100,
    description: '删除订单',
    ip: '10.0.0.1',
    created_at: '2026-03-20T11:30:00Z',
  },
]

const mockDetail: OperationLogDetail = {
  id: 1,
  user_id: 1,
  action: 'user.create',
  target_type: 'user',
  target_id: 5,
  description: '创建用户 test_user',
  ip: '127.0.0.1',
  created_at: '2026-03-20T10:30:00Z',
  detail: { before: { role: 'viewer' }, after: { role: 'editor' } },
}

const mockUsers = {
  items: [
    { id: 1, username: 'admin' },
    { id: 2, username: 'editor' },
  ],
  total: 2,
  page: 1,
  per_page: 200,
}

const mockRoles = {
  items: [
    { id: 3, name: 'developer' },
    { id: 4, name: 'administrator' },
  ],
  total: 2,
  page: 1,
  per_page: 200,
}

const mockRouteGroups = {
  items: [
    { id: 10, name: 'default' },
    { id: 11, name: 'api-management' },
  ],
  total: 2,
  page: 1,
  per_page: 200,
}

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

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

  describe('initial state', () => {
    it('S1: starts with correct initial state', () => {
      const store = useOperationLogStore()
      expect(store.logs).toEqual([])
      expect(store.loading).toBe(false)
      expect(store.pagination.page).toBe(1)
      expect(store.pagination.per_page).toBe(20)
      expect(store.pagination.total).toBe(0)
      expect(store.filters.user_id).toBeUndefined()
      expect(store.filters.action).toBeUndefined()
      expect(store.filters.target_type).toBeUndefined()
      expect(store.filters.created_at_range).toBeNull()
    })
  })

  describe('fetchLogs', () => {
    it('S2: fetches logs successfully', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get).mockResolvedValueOnce({
        items: mockLogs,
        total: 3,
        page: 1,
        per_page: 20,
      })

      const store = useOperationLogStore()
      await store.fetchLogs()

      expect(api.get).toHaveBeenCalledWith('/api/v1/logs/operations', {
        page: 1,
        per_page: 20,
        user_id: undefined,
        action: undefined,
        target_type: undefined,
        created_at_from: undefined,
        created_at_to: undefined,
      })
      expect(store.logs).toEqual(mockLogs)
      expect(store.pagination.total).toBe(3)
    })

    it('S3: fetches with filters applied', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get).mockResolvedValueOnce({
        items: [mockLogs[0]],
        total: 1,
        page: 1,
        per_page: 20,
      })

      const store = useOperationLogStore()
      store.filters.action = 'user.create'
      store.filters.target_type = 'user'
      store.filters.user_id = 1
      store.filters.created_at_range = ['2026-03-19', '2026-03-20']
      await store.fetchLogs()

      expect(api.get).toHaveBeenCalledWith('/api/v1/logs/operations', {
        page: 1,
        per_page: 20,
        user_id: 1,
        action: 'user.create',
        target_type: 'user',
        created_at_from: '2026-03-19',
        created_at_to: '2026-03-20',
      })
      expect(store.logs).toHaveLength(1)
    })

    it('S4: handles fetch failure gracefully', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get).mockRejectedValueOnce(new Error('网络超时'))

      const store = useOperationLogStore()
      await store.fetchLogs()

      expect(store.logs).toEqual([])
      expect(store.pagination.total).toBe(0)
      expect(store.loading).toBe(false)
    })
  })

  describe('resetFilters', () => {
    it('S5: resets all filters and page', () => {
      const store = useOperationLogStore()
      store.filters.action = 'user.create'
      store.filters.target_type = 'user'
      store.filters.user_id = 1
      store.filters.created_at_range = ['2026-03-01', '2026-03-20']
      store.pagination.page = 3

      store.resetFilters()

      expect(store.filters.action).toBeUndefined()
      expect(store.filters.target_type).toBeUndefined()
      expect(store.filters.user_id).toBeUndefined()
      expect(store.filters.created_at_range).toBeNull()
      expect(store.pagination.page).toBe(1)
    })
  })

  describe('lookup maps', () => {
    it('S6: builds all maps from loaded lookups', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get)
        .mockResolvedValueOnce(mockUsers)
        .mockResolvedValueOnce(mockRoles)
        .mockResolvedValueOnce(mockRouteGroups)

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

      expect(store.userMap.get(1)).toBe('admin')
      expect(store.userMap.get(2)).toBe('editor')
      expect(store.roleMap.get(3)).toBe('developer')
      expect(store.roleMap.get(4)).toBe('administrator')
      expect(store.routeGroupMap.get(10)).toBe('default')
      expect(store.routeGroupMap.get(11)).toBe('api-management')
    })

    it('S6a: userMap handles string id from API via Number() defense', () => {
      const store = useOperationLogStore()
      store.users = [{ id: '1' as any, username: 'admin' }] as any
      expect(store.userMap.get(1)).toBe('admin')
    })

    it('S7: handles all lookups failure gracefully', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get)
        .mockRejectedValueOnce(new Error('用户加载失败'))
        .mockRejectedValueOnce(new Error('角色加载失败'))
        .mockRejectedValueOnce(new Error('路由组加载失败'))

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

      expect(store.userMap.size).toBe(0)
      expect(store.roleMap.size).toBe(0)
      expect(store.routeGroupMap.size).toBe(0)
    })

    it('S7a: partial lookup failure does not affect other maps', async () => {
      const { api } = await import('@/utils/request')
      vi.mocked(api.get)
        .mockResolvedValueOnce(mockUsers)
        .mockRejectedValueOnce(new Error('角色加载失败'))
        .mockResolvedValueOnce(mockRouteGroups)

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

      expect(store.userMap.get(1)).toBe('admin')
      expect(store.roleMap.size).toBe(0)
      expect(store.routeGroupMap.get(10)).toBe('default')
    })
  })
})

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

describe('OperationLogPage', () => {
  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/users') return Promise.resolve(mockUsers)
      if (url === '/api/v1/roles') return Promise.resolve(mockRoles)
      if (url === '/api/v1/route-groups') return Promise.resolve(mockRouteGroups)
      if (url === '/api/v1/logs/operations') {
        return Promise.resolve({
          items: mockLogs,
          total: 3,
          page: 1,
          per_page: 20,
        })
      }
      if (url.startsWith('/api/v1/logs/operations/')) return Promise.resolve(mockDetail)
      return Promise.resolve(null)
    })

    const { default: OperationLogPage } = await import('../operations.vue')
    wrapper = mount(OperationLogPage, {
      attachTo: document.body,
      global: {
        stubs: {
          SearchOutlined: { template: '<span />' },
          ReloadOutlined: { template: '<span />' },
          EyeOutlined: { template: '<span />' },
        },
      },
    })
    await flushPromises()
    await nextTick()
    return { api }
  }

  it('P1: renders page title', async () => {
    await mountPage()
    expect(wrapper.text()).toContain('操作日志')
  }, 15000)

  it('P2: calls API on mount', async () => {
    const { api } = await mountPage()
    expect(api.get).toHaveBeenCalledWith('/api/v1/users', expect.any(Object))
    expect(api.get).toHaveBeenCalledWith('/api/v1/logs/operations', expect.any(Object))
  })

  it('P3: renders table with log data', async () => {
    await mountPage()
    const html = wrapper.html()
    expect(html).toContain('创建用户 test_user')
    expect(html).toContain('更新角色权限')
    expect(html).toContain('删除订单')
  })

  it('P4: renders action as colored tags', async () => {
    await mountPage()
    const tags = wrapper.findAll('.ant-tag')
    const tagTexts = tags.map((t) => t.text())
    expect(tagTexts).toContain('user.create')
    expect(tagTexts).toContain('role.update')
    expect(tagTexts).toContain('order.delete')
  })

  it('P5: displays target with name for small datasets, #id for large', async () => {
    await mountPage()
    const html = wrapper.html()
    // user id=5 not in mockUsers, so fallback to #id
    expect(html).toContain('用户 #5')
    // role id=3 is in mockRoles as 'developer'
    expect(html).toContain('角色 developer')
    // order is large dataset, always #id
    expect(html).toContain('订单 #100')
  })

  it('P6: formats created_at as YYYY-MM-DD HH:mm', async () => {
    await mountPage()
    const html = wrapper.html()
    expect(html).toContain('2026-03-20 10:30')
    expect(html).toContain('2026-03-20 11:00')
  })

  it('P7: search button triggers fetch with page=1', async () => {
    const { api } = await mountPage()
    const store = useOperationLogStore()
    store.pagination.page = 3
    vi.mocked(api.get).mockClear()
    vi.mocked(api.get).mockResolvedValue({
      items: [],
      total: 0,
      page: 1,
      per_page: 20,
    })

    const buttons = document.body.querySelectorAll('button')
    const searchBtn = Array.from(buttons).find((b) => b.textContent?.trim().includes('搜索'))
    expect(searchBtn).toBeDefined()

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

    expect(store.pagination.page).toBe(1)
    expect(api.get).toHaveBeenCalledWith('/api/v1/logs/operations', expect.any(Object))
  })

  it('P8: reset button clears filters and fetches', async () => {
    const { api } = await mountPage()
    const store = useOperationLogStore()
    store.filters.action = 'user.create'
    store.filters.target_type = 'user'
    vi.mocked(api.get).mockClear()
    vi.mocked(api.get).mockResolvedValue({
      items: [],
      total: 0,
      page: 1,
      per_page: 20,
    })

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

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

    expect(store.filters.action).toBeUndefined()
    expect(store.filters.target_type).toBeUndefined()
    expect(api.get).toHaveBeenCalled()
  })

  it('P9a: renders operator username instead of #id', async () => {
    await mountPage()
    const html = wrapper.html()
    expect(html).toContain('admin')
    expect(html).not.toMatch(/#1(?![\d])/)
  })

  it('P9b: renders role name in target column', async () => {
    await mountPage()
    const html = wrapper.html()
    // mockLogs[1] has target_type='role', target_id=3, which maps to 'developer'
    expect(html).toContain('角色 developer')
    expect(html).not.toContain('角色 #3')
  })

  it('P9c: preserves #id for large dataset targets', async () => {
    await mountPage()
    const html = wrapper.html()
    // mockLogs[2] has target_type='order', target_id=100 — large dataset, keep #id
    expect(html).toContain('订单 #100')
  })

  it('P9d: calls all three lookup APIs on mount', async () => {
    const { api } = await mountPage()
    expect(api.get).toHaveBeenCalledWith('/api/v1/users', { per_page: 200 })
    expect(api.get).toHaveBeenCalledWith('/api/v1/roles', { per_page: 200 })
    expect(api.get).toHaveBeenCalledWith('/api/v1/route-groups', { per_page: 200 })
  })

  it('P9: view button opens drawer and loads detail', async () => {
    const { api } = await mountPage()

    const viewButtons = document.body.querySelectorAll('.ant-btn-link')
    expect(viewButtons.length).toBeGreaterThan(0)

    viewButtons[0]!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
    await flushPromises()
    await nextTick()

    expect(api.get).toHaveBeenCalledWith('/api/v1/logs/operations/1')
  })
})
