import type { MockedFunction } from 'vitest'
import type { CustomFile as File } from '@/models/datasets'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fetchFilePreview } from '@/service/common'
import FilePreview from '../index'

// Mock the fetchFilePreview service
vi.mock('@/service/common', () => ({
  fetchFilePreview: vi.fn(),
}))

const mockFetchFilePreview = fetchFilePreview as MockedFunction<typeof fetchFilePreview>

// Factory function to create mock file objects
const createMockFile = (overrides: Partial<File> = {}): File => {
  const fileName = overrides.name ?? 'test-file.txt'
  // Create a plain object that looks like a File with CustomFile properties
  // We can't use Object.assign on a real File because 'name' is a getter-only property
  return {
    name: fileName,
    size: 1024,
    type: 'text/plain',
    lastModified: Date.now(),
    id: 'file-123',
    extension: 'txt',
    mime_type: 'text/plain',
    created_by: 'user-1',
    created_at: Date.now(),
    ...overrides,
  } as File
}

// Helper to render FilePreview with default props
const renderFilePreview = (props: Partial<{ file?: File, hidePreview: () => void }> = {}) => {
  const defaultProps = {
    file: createMockFile(),
    hidePreview: vi.fn(),
    ...props,
  }
  return {
    ...render(<FilePreview {...defaultProps} />),
    props: defaultProps,
  }
}

// Helper to find the loading spinner element
const findLoadingSpinner = (container: HTMLElement) => {
  return container.querySelector('.spin-animation')
}

// FilePreview Component Tests
describe('FilePreview', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    // Default successful API response
    mockFetchFilePreview.mockResolvedValue({ content: 'Preview content here' })
  })

  // Rendering Tests - Verify component renders properly
  describe('Rendering', () => {
    it('should render without crashing', async () => {
      renderFilePreview()

      await waitFor(() => {
        expect(screen.getByText('datasetCreation.stepOne.filePreview'))!.toBeInTheDocument()
      })
    })

    it('should render file preview header', async () => {
      renderFilePreview()

      expect(screen.getByText('datasetCreation.stepOne.filePreview'))!.toBeInTheDocument()
    })

    it('should render close button with XMarkIcon', async () => {
      renderFilePreview()

      const closeButton = screen.getByRole('button', { name: /operation\.close$/ })
      expect(closeButton)!.toBeInTheDocument()
      const xMarkIcon = closeButton?.querySelector('svg')
      expect(xMarkIcon)!.toBeInTheDocument()
    })

    it('should render file name without extension', async () => {
      const file = createMockFile({ name: 'document.pdf' })

      renderFilePreview({ file })

      await waitFor(() => {
        expect(screen.getByText('document'))!.toBeInTheDocument()
      })
    })

    it('should render file extension', async () => {
      const file = createMockFile({ extension: 'pdf' })

      renderFilePreview({ file })

      expect(screen.getByText('.pdf'))!.toBeInTheDocument()
    })

    it('should apply correct CSS classes to container', async () => {
      const { container } = renderFilePreview()

      const wrapper = container.firstChild as HTMLElement
      expect(wrapper)!.toHaveClass('h-full')
    })
  })

  // Loading State Tests
  describe('Loading State', () => {
    it('should show loading indicator initially', async () => {
      // Arrange - Delay API response to keep loading state
      mockFetchFilePreview.mockImplementation(
        () => new Promise(resolve => setTimeout(() => resolve({ content: 'test' }), 100)),
      )

      const { container } = renderFilePreview()

      // Assert - Loading should be visible initially (using spin-animation class)
      const loadingElement = findLoadingSpinner(container)
      expect(loadingElement)!.toBeInTheDocument()
    })

    it('should hide loading indicator after content loads', async () => {
      mockFetchFilePreview.mockResolvedValue({ content: 'Loaded content' })

      const { container } = renderFilePreview()

      await waitFor(() => {
        expect(screen.getByText('Loaded content'))!.toBeInTheDocument()
      })
      // Loading should be gone
      const loadingElement = findLoadingSpinner(container)
      expect(loadingElement).not.toBeInTheDocument()
    })

    it('should show loading when file changes', async () => {
      const file1 = createMockFile({ id: 'file-1', name: 'file1.txt' })
      const file2 = createMockFile({ id: 'file-2', name: 'file2.txt' })

      let resolveFirst: (value: { content: string }) => void
      let resolveSecond: (value: { content: string }) => void

      mockFetchFilePreview
        .mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve }))
        .mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))

      // Act - Initial render
      const { rerender, container } = render(
        <FilePreview file={file1} hidePreview={vi.fn()} />,
      )

      // First file loading - spinner should be visible
      // First file loading - spinner should be visible
      expect(findLoadingSpinner(container))!.toBeInTheDocument()

      // Resolve first file
      await act(async () => {
        resolveFirst({ content: 'Content 1' })
      })

      await waitFor(() => {
        expect(screen.getByText('Content 1'))!.toBeInTheDocument()
      })

      // Rerender with new file
      rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)

      // Should show loading again
      await waitFor(() => {
        expect(findLoadingSpinner(container))!.toBeInTheDocument()
      })

      // Resolve second file
      await act(async () => {
        resolveSecond({ content: 'Content 2' })
      })

      await waitFor(() => {
        expect(screen.getByText('Content 2'))!.toBeInTheDocument()
      })
    })
  })

  // API Call Tests
  describe('API Calls', () => {
    it('should call fetchFilePreview with correct fileID', async () => {
      const file = createMockFile({ id: 'test-file-id' })

      renderFilePreview({ file })

      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledWith({ fileID: 'test-file-id' })
      })
    })

    it('should not call fetchFilePreview when file is undefined', async () => {
      renderFilePreview({ file: undefined })

      expect(mockFetchFilePreview).not.toHaveBeenCalled()
    })

    it('should not call fetchFilePreview when file has no id', async () => {
      const file = createMockFile({ id: undefined })

      renderFilePreview({ file })

      expect(mockFetchFilePreview).not.toHaveBeenCalled()
    })

    it('should call fetchFilePreview again when file changes', async () => {
      const file1 = createMockFile({ id: 'file-1' })
      const file2 = createMockFile({ id: 'file-2' })

      const { rerender } = render(
        <FilePreview file={file1} hidePreview={vi.fn()} />,
      )

      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledWith({ fileID: 'file-1' })
      })

      rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)

      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledWith({ fileID: 'file-2' })
        expect(mockFetchFilePreview).toHaveBeenCalledTimes(2)
      })
    })

    it('should handle API success and display content', async () => {
      mockFetchFilePreview.mockResolvedValue({ content: 'File preview content from API' })

      renderFilePreview()

      await waitFor(() => {
        expect(screen.getByText('File preview content from API'))!.toBeInTheDocument()
      })
    })

    it('should handle API error gracefully', async () => {
      mockFetchFilePreview.mockRejectedValue(new Error('Network error'))

      const { container } = renderFilePreview()

      // Assert - Component should not crash, loading may persist
      await waitFor(() => {
        expect(container.firstChild)!.toBeInTheDocument()
      })
      // No error thrown, component still rendered
      // No error thrown, component still rendered
      expect(screen.getByText('datasetCreation.stepOne.filePreview'))!.toBeInTheDocument()
    })

    it('should handle empty content response', async () => {
      mockFetchFilePreview.mockResolvedValue({ content: '' })

      const { container } = renderFilePreview()

      // Assert - Should still render without loading
      await waitFor(() => {
        const loadingElement = findLoadingSpinner(container)
        expect(loadingElement).not.toBeInTheDocument()
      })
    })
  })

  describe('User Interactions', () => {
    it('should call hidePreview when close button is clicked', async () => {
      const hidePreview = vi.fn()
      renderFilePreview({ hidePreview })

      fireEvent.click(screen.getByRole('button', { name: /operation\.close$/ }))

      expect(hidePreview).toHaveBeenCalledTimes(1)
    })

    it('should call hidePreview with event object when clicked', async () => {
      const hidePreview = vi.fn()
      renderFilePreview({ hidePreview })

      fireEvent.click(screen.getByRole('button', { name: /operation\.close$/ }))

      // Assert - onClick receives the event object
      expect(hidePreview).toHaveBeenCalled()
      expect(hidePreview.mock.calls[0]![0]).toBeDefined()
    })

    it('should handle multiple clicks on close button', async () => {
      const hidePreview = vi.fn()
      renderFilePreview({ hidePreview })

      const closeButton = screen.getByRole('button', { name: /operation\.close$/ })
      fireEvent.click(closeButton)
      fireEvent.click(closeButton)
      fireEvent.click(closeButton)

      expect(hidePreview).toHaveBeenCalledTimes(3)
    })
  })

  describe('State Management', () => {
    it('should initialize with loading state true', async () => {
      // Arrange - Keep loading indefinitely (never resolves)
      mockFetchFilePreview.mockImplementation(() => new Promise(() => { /* intentionally empty */ }))

      const { container } = renderFilePreview()

      const loadingElement = findLoadingSpinner(container)
      expect(loadingElement)!.toBeInTheDocument()
    })

    it('should update previewContent state after successful fetch', async () => {
      mockFetchFilePreview.mockResolvedValue({ content: 'New preview content' })

      renderFilePreview()

      await waitFor(() => {
        expect(screen.getByText('New preview content'))!.toBeInTheDocument()
      })
    })

    it('should reset loading to true when file changes', async () => {
      const file1 = createMockFile({ id: 'file-1' })
      const file2 = createMockFile({ id: 'file-2' })

      mockFetchFilePreview
        .mockResolvedValueOnce({ content: 'Content 1' })
        .mockImplementationOnce(() => new Promise(() => { /* never resolves */ }))

      const { rerender, container } = render(
        <FilePreview file={file1} hidePreview={vi.fn()} />,
      )

      await waitFor(() => {
        expect(screen.getByText('Content 1'))!.toBeInTheDocument()
      })

      // Change file
      rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)

      // Assert - Loading should be shown again
      await waitFor(() => {
        const loadingElement = findLoadingSpinner(container)
        expect(loadingElement)!.toBeInTheDocument()
      })
    })

    it('should preserve content until new content loads', async () => {
      const file1 = createMockFile({ id: 'file-1' })
      const file2 = createMockFile({ id: 'file-2' })

      let resolveSecond: (value: { content: string }) => void

      mockFetchFilePreview
        .mockResolvedValueOnce({ content: 'Content 1' })
        .mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))

      const { rerender } = render(
        <FilePreview file={file1} hidePreview={vi.fn()} />,
      )

      await waitFor(() => {
        expect(screen.getByText('Content 1'))!.toBeInTheDocument()
      })

      // Change file - loading should replace content
      rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)

      // Resolve second fetch
      await act(async () => {
        resolveSecond({ content: 'Content 2' })
      })

      await waitFor(() => {
        expect(screen.getByText('Content 2'))!.toBeInTheDocument()
        expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
      })
    })
  })

  describe('Props', () => {
    describe('file prop', () => {
      it('should render correctly with file prop', async () => {
        const file = createMockFile({ name: 'my-document.pdf', extension: 'pdf' })

        renderFilePreview({ file })

        expect(screen.getByText('my-document'))!.toBeInTheDocument()
        expect(screen.getByText('.pdf'))!.toBeInTheDocument()
      })

      it('should render correctly without file prop', async () => {
        renderFilePreview({ file: undefined })

        // Assert - Header should still render
        // Assert - Header should still render
        expect(screen.getByText('datasetCreation.stepOne.filePreview'))!.toBeInTheDocument()
      })

      it('should handle file with multiple dots in name', async () => {
        const file = createMockFile({ name: 'my.document.v2.pdf' })

        renderFilePreview({ file })

        // Assert - Should join all parts except last with comma
        // Assert - Should join all parts except last with comma
        expect(screen.getByText('my,document,v2'))!.toBeInTheDocument()
      })

      it('should handle file with no extension in name', async () => {
        const file = createMockFile({ name: 'README' })

        const { container } = renderFilePreview({ file })

        // Assert - getFileName returns empty for single segment, but component still renders
        const fileNameElement = container.querySelector('[class*="fileName"]')
        expect(fileNameElement)!.toBeInTheDocument()
        // The first span (file name) should be empty
        const fileNameSpan = fileNameElement?.querySelector('span:first-child')
        expect(fileNameSpan?.textContent).toBe('')
      })

      it('should handle file with empty name', async () => {
        const file = createMockFile({ name: '' })

        const { container } = renderFilePreview({ file })

        // Assert - Should not crash
        // Assert - Should not crash
        expect(container.firstChild)!.toBeInTheDocument()
      })
    })

    describe('hidePreview prop', () => {
      it('should accept hidePreview callback', async () => {
        const hidePreview = vi.fn()

        renderFilePreview({ hidePreview })

        // Assert - No errors thrown
        // Assert - No errors thrown
        expect(screen.getByText('datasetCreation.stepOne.filePreview'))!.toBeInTheDocument()
      })
    })
  })

  describe('Edge Cases', () => {
    it('should handle file with undefined id', async () => {
      const file = createMockFile({ id: undefined })

      const { container } = renderFilePreview({ file })

      // Assert - Should not call API, remain in loading state
      expect(mockFetchFilePreview).not.toHaveBeenCalled()
      expect(container.firstChild)!.toBeInTheDocument()
    })

    it('should handle file with empty string id', async () => {
      const file = createMockFile({ id: '' })

      renderFilePreview({ file })

      // Assert - Empty string is falsy, should not call API
      expect(mockFetchFilePreview).not.toHaveBeenCalled()
    })

    it('should handle very long file names', async () => {
      const longName = `${'a'.repeat(200)}.pdf`
      const file = createMockFile({ name: longName })

      renderFilePreview({ file })

      expect(screen.getByText('a'.repeat(200)))!.toBeInTheDocument()
    })

    it('should handle file with special characters in name', async () => {
      const file = createMockFile({ name: 'file-with_special@#$%.txt' })

      renderFilePreview({ file })

      expect(screen.getByText('file-with_special@#$%'))!.toBeInTheDocument()
    })

    it('should handle very long preview content', async () => {
      const longContent = 'x'.repeat(10000)
      mockFetchFilePreview.mockResolvedValue({ content: longContent })

      renderFilePreview()

      await waitFor(() => {
        expect(screen.getByText(longContent))!.toBeInTheDocument()
      })
    })

    it('should handle preview content with special characters safely', async () => {
      const specialContent = '<script>alert("xss")</script>\n\t& < > "'
      mockFetchFilePreview.mockResolvedValue({ content: specialContent })

      const { container } = renderFilePreview()

      // Assert - Should render as text, not execute scripts
      await waitFor(() => {
        const contentDiv = container.querySelector('[class*="fileContent"]')
        expect(contentDiv)!.toBeInTheDocument()
        // Content is escaped by React, so HTML entities are displayed
        expect(contentDiv?.textContent).toContain('alert')
      })
    })

    it('should handle preview content with unicode', async () => {
      const unicodeContent = '中文内容 🚀 émojis & spëcîal çhàrs'
      mockFetchFilePreview.mockResolvedValue({ content: unicodeContent })

      renderFilePreview()

      await waitFor(() => {
        expect(screen.getByText(unicodeContent))!.toBeInTheDocument()
      })
    })

    it('should handle preview content with newlines', async () => {
      const multilineContent = 'Line 1\nLine 2\nLine 3'
      mockFetchFilePreview.mockResolvedValue({ content: multilineContent })

      const { container } = renderFilePreview()

      // Assert - Content should be in the DOM
      await waitFor(() => {
        const contentDiv = container.querySelector('[class*="fileContent"]')
        expect(contentDiv)!.toBeInTheDocument()
        expect(contentDiv?.textContent).toContain('Line 1')
        expect(contentDiv?.textContent).toContain('Line 2')
        expect(contentDiv?.textContent).toContain('Line 3')
      })
    })

    it('should handle null content from API', async () => {
      mockFetchFilePreview.mockResolvedValue({ content: null as unknown as string })

      const { container } = renderFilePreview()

      // Assert - Should not crash
      await waitFor(() => {
        expect(container.firstChild)!.toBeInTheDocument()
      })
    })
  })

  // Side Effects and Cleanup Tests
  describe('Side Effects and Cleanup', () => {
    it('should trigger effect when file prop changes', async () => {
      const file1 = createMockFile({ id: 'file-1' })
      const file2 = createMockFile({ id: 'file-2' })

      const { rerender } = render(
        <FilePreview file={file1} hidePreview={vi.fn()} />,
      )

      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledTimes(1)
      })

      rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)

      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledTimes(2)
      })
    })

    it('should not trigger effect when hidePreview changes', async () => {
      const file = createMockFile()
      const hidePreview1 = vi.fn()
      const hidePreview2 = vi.fn()

      const { rerender } = render(
        <FilePreview file={file} hidePreview={hidePreview1} />,
      )

      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledTimes(1)
      })

      rerender(<FilePreview file={file} hidePreview={hidePreview2} />)

      // Assert - Should not call API again (file didn't change)
      // Note: This depends on useEffect dependency array only including [file]
      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledTimes(1)
      })
    })

    it('should handle rapid file changes', async () => {
      const files = Array.from({ length: 5 }, (_, i) =>
        createMockFile({ id: `file-${i}` }))

      const { rerender } = render(
        <FilePreview file={files[0]} hidePreview={vi.fn()} />,
      )

      // Rapidly change files
      for (let i = 1; i < files.length; i++)
        rerender(<FilePreview file={files[i]} hidePreview={vi.fn()} />)

      // Assert - Should have called API for each file
      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledTimes(5)
      })
    })

    it('should handle unmount during loading', async () => {
      mockFetchFilePreview.mockImplementation(
        () => new Promise(resolve => setTimeout(() => resolve({ content: 'delayed' }), 1000)),
      )

      const { unmount } = renderFilePreview()

      // Unmount before API resolves
      unmount()

      // Assert - No errors should be thrown (React handles state updates on unmounted)
      expect(true).toBe(true)
    })

    it('should handle file changing from defined to undefined', async () => {
      const file = createMockFile()

      const { rerender, container } = render(
        <FilePreview file={file} hidePreview={vi.fn()} />,
      )

      await waitFor(() => {
        expect(mockFetchFilePreview).toHaveBeenCalledTimes(1)
      })

      rerender(<FilePreview file={undefined} hidePreview={vi.fn()} />)

      // Assert - Should not crash, API should not be called again
      // Assert - Should not crash, API should not be called again
      expect(container.firstChild)!.toBeInTheDocument()
      expect(mockFetchFilePreview).toHaveBeenCalledTimes(1)
    })
  })

  // getFileName Helper Tests
  describe('getFileName Helper', () => {
    it('should extract name without extension for simple filename', async () => {
      const file = createMockFile({ name: 'document.pdf' })

      renderFilePreview({ file })

      expect(screen.getByText('document'))!.toBeInTheDocument()
    })

    it('should handle filename with multiple dots', async () => {
      const file = createMockFile({ name: 'file.name.with.dots.txt' })

      renderFilePreview({ file })

      // Assert - Should join all parts except last with comma
      // Assert - Should join all parts except last with comma
      expect(screen.getByText('file,name,with,dots'))!.toBeInTheDocument()
    })

    it('should return empty for filename without dot', async () => {
      const file = createMockFile({ name: 'nodotfile' })

      const { container } = renderFilePreview({ file })

      // Assert - slice(0, -1) on single element array returns empty
      const fileNameElement = container.querySelector('[class*="fileName"]')
      const firstSpan = fileNameElement?.querySelector('span:first-child')
      expect(firstSpan?.textContent).toBe('')
    })

    it('should return empty string when file is undefined', async () => {
      const { container } = renderFilePreview({ file: undefined })

      // Assert - File name area should have empty first span
      const fileNameElement = container.querySelector('.system-xs-medium')
      expect(fileNameElement)!.toBeInTheDocument()
    })
  })

  describe('Accessibility', () => {
    it('should have clickable close button with visual indicator', async () => {
      const { container } = renderFilePreview()

      const closeButton = container.querySelector('.cursor-pointer')
      expect(closeButton)!.toBeInTheDocument()
      expect(closeButton)!.toHaveClass('cursor-pointer')
    })

    it('should have proper heading structure', async () => {
      renderFilePreview()

      expect(screen.getByText('datasetCreation.stepOne.filePreview'))!.toBeInTheDocument()
    })
  })

  // Error Handling Tests
  describe('Error Handling', () => {
    it('should not crash on API network error', async () => {
      mockFetchFilePreview.mockRejectedValue(new Error('Network Error'))

      const { container } = renderFilePreview()

      // Assert - Component should still render
      await waitFor(() => {
        expect(container.firstChild)!.toBeInTheDocument()
      })
    })

    it('should not crash on API timeout', async () => {
      mockFetchFilePreview.mockRejectedValue(new Error('Timeout'))

      const { container } = renderFilePreview()

      await waitFor(() => {
        expect(container.firstChild)!.toBeInTheDocument()
      })
    })

    it('should not crash on malformed API response', async () => {
      mockFetchFilePreview.mockResolvedValue({} as { content: string })

      const { container } = renderFilePreview()

      await waitFor(() => {
        expect(container.firstChild)!.toBeInTheDocument()
      })
    })
  })
})
