import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test'

const successMock = mock(() => {})
const errorMock = mock(() => {})

globalThis.useNotificationStore = () => ({
  add: () => 'test-id',
  markRead: () => {},
}) as any

mock.module('~/composables/useNotification', () => ({
  useNotification: () => ({ success: successMock, error: errorMock }),
  default: () => ({ success: successMock, error: errorMock }),
}))

mock.module('~/stores/notification', () => ({
  useNotificationStore: () => ({
    add: () => 'test-id',
    markRead: () => {},
  }),
}))

// eslint-disable-next-line import/first
import * as utils from '~/utils/index'
// eslint-disable-next-line import/first
import { useTasks } from '~/composables/useTasks'
// eslint-disable-next-line import/first
import type { Task, TaskInspectResponse, TaskMetadataResponse, Pagination } from '~/types/tasks'

const mockTask: Task = {
  id: 1,
  name: 'Test Task',
  url: 'https://www.youtube.com/channel/UCtest123',
  folder: 'youtube/Test',
  preset: 'default',
  timer: '0 12 * * *',
  template: '%(title)s.%(ext)s',
  cli: '--format best',
  auto_start: true,
  handler_enabled: true,
  enabled: true,
  created_at: '2024-01-01T00:00:00Z',
  updated_at: '2024-01-01T00:00:00Z',
}

const mockPagination: Pagination = {
  page: 1,
  per_page: 50,
  total: 1,
  total_pages: 1,
  has_next: false,
  has_prev: false,
}

function createMockResponse({ ok, status, jsonData }: { ok: boolean; status: number; jsonData: any }) {
  return {
    ok,
    status,
    headers: new Headers(),
    redirected: false,
    statusText: '',
    type: 'basic',
    url: '',
    body: null,
    bodyUsed: false,
    clone() {
      return this
    },
    async json() {
      return jsonData
    },
    text: async () => JSON.stringify(jsonData),
    arrayBuffer: async () => new ArrayBuffer(0),
    blob: async () => new Blob(),
    formData: async () => new FormData(),
  } as Response
}

describe('useTasks', () => {
  beforeEach(() => {
    successMock.mockClear()
    errorMock.mockClear()
    const tasks = useTasks()
    tasks.__resetForTesting()
  })

  describe('loadTasks', () => {
    it('loads tasks with pagination successfully', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: {
            items: [mockTask],
            pagination: mockPagination,
          },
        }),
      )

      const tasks = useTasks()
      await tasks.loadTasks()

      expect(tasks.tasks.value).toHaveLength(1)
      expect(tasks.tasks.value[0]).toEqual(mockTask)
      expect(tasks.pagination.value).toEqual(mockPagination)
      expect(tasks.lastError.value).toBeNull()
      requestSpy.mockRestore()
    })

    it('sorts tasks by name A-Z', async () => {
      const items = [
        { ...mockTask, id: 1, name: 'Zebra' },
        { ...mockTask, id: 2, name: 'Alpha' },
        { ...mockTask, id: 3, name: 'Beta' },
      ]

      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: {
            items,
            pagination: mockPagination,
          },
        }),
      )

      const tasks = useTasks()
      await tasks.loadTasks()

      const sorted = tasks.tasks.value
      expect(sorted[0].name).toBe('Alpha')
      expect(sorted[1].name).toBe('Beta')
      expect(sorted[2].name).toBe('Zebra')
      requestSpy.mockRestore()
    })

    it('handles empty tasks list', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: {
            items: [],
            pagination: { ...mockPagination, total: 0 },
          },
        }),
      )

      const tasks = useTasks()
      await tasks.loadTasks()

      expect(tasks.tasks.value).toEqual([])
      expect(tasks.lastError.value).toBeNull()
      requestSpy.mockRestore()
    })

    it('handles errors during load', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 500,
          jsonData: { error: 'Server error' },
        }),
      )

      const tasks = useTasks()
      await tasks.loadTasks()

      expect(tasks.lastError.value).toBeTruthy()
      requestSpy.mockRestore()
    })

    it('respects page and per_page parameters', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: {
            items: [mockTask],
            pagination: { ...mockPagination, page: 2 },
          },
        }),
      )

      const tasks = useTasks()
      await tasks.loadTasks(2, 25)

      expect(requestSpy).toHaveBeenCalledWith('/api/tasks/?page=2&per_page=25')
      requestSpy.mockRestore()
    })
  })

  describe('getTask', () => {
    it('fetches a single task successfully', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: mockTask,
        }),
      )

      const tasks = useTasks()
      const result = await tasks.getTask(1)

      expect(result).toEqual(mockTask)
      expect(tasks.lastError.value).toBeNull()
      requestSpy.mockRestore()
    })

    it('handles 404 not found', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 404,
          jsonData: { error: 'Task not found' },
        }),
      )

      const tasks = useTasks()
      const result = await tasks.getTask(999)

      expect(result).toBeNull()
      expect(tasks.lastError.value).toBeTruthy()
      requestSpy.mockRestore()
    })
  })

  describe('createTask', () => {
    it('creates a task successfully', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: mockTask,
        }),
      )

      const tasks = useTasks()
      const initialTotal = tasks.pagination.value.total
      const newTask = { ...mockTask }
      delete (newTask as any).id

      const result = await tasks.createTask(newTask)

      expect(result).toEqual(mockTask)
      expect(tasks.tasks.value).toContainEqual(mockTask)
      expect(tasks.pagination.value.total).toBe(initialTotal + 1)
      requestSpy.mockRestore()
    })

    it('calls callback on success', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: mockTask,
        }),
      )

      const callback = mock(() => {})
      const tasks = useTasks()
      const newTask = { ...mockTask }
      delete (newTask as any).id

      await tasks.createTask(newTask, callback)

      expect(callback).toHaveBeenCalledWith({
        success: true,
        error: null,
        detail: null,
        data: mockTask,
      })
      requestSpy.mockRestore()
    })

    it('calls callback on error', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 400,
          jsonData: { detail: [{ loc: ['name'], msg: 'Field required', type: 'value_error' }] },
        }),
      )

      const callback = mock(() => {})
      const tasks = useTasks()
      const newTask = { ...mockTask }
      delete (newTask as any).id

      await tasks.createTask(newTask, callback)

      expect(callback).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          error: expect.any(String),
        }),
      )
      requestSpy.mockRestore()
    })
  })

  describe('updateTask', () => {
    it('updates a task successfully', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: { ...mockTask, name: 'Updated Name' },
        }),
      )

      const tasks = useTasks()
      const updated = { ...mockTask, name: 'Updated Name' }

      const result = await tasks.updateTask(1, updated)

      expect(result?.name).toBe('Updated Name')
      requestSpy.mockRestore()
    })

    it('calls callback on success', async () => {
      const updatedTask = { ...mockTask, name: 'Updated' }
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: updatedTask,
        }),
      )

      const callback = mock(() => {})
      const tasks = useTasks()

      await tasks.updateTask(1, updatedTask, callback)

      expect(callback).toHaveBeenCalledWith({
        success: true,
        error: null,
        detail: null,
        data: updatedTask,
      })
      requestSpy.mockRestore()
    })

    it('removes id field from task before sending', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: mockTask,
        }),
      )

      const tasks = useTasks()
      const taskWithId = { ...mockTask }
      await tasks.updateTask(1, taskWithId)

      const requestBody = JSON.parse((requestSpy.mock.calls[0][1] as any).body)
      expect('id' in requestBody).toBe(false)
      requestSpy.mockRestore()
    })
  })

  describe('patchTask', () => {
    it('patches a task successfully', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: { ...mockTask, enabled: false },
        }),
      )

      const tasks = useTasks()
      const result = await tasks.patchTask(1, { enabled: false })

      expect(result?.enabled).toBe(false)
      requestSpy.mockRestore()
    })

    it('calls callback on patch success', async () => {
      const patchedTask = { ...mockTask, enabled: false }
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: patchedTask,
        }),
      )

      const callback = mock(() => {})
      const tasks = useTasks()

      await tasks.patchTask(1, { enabled: false }, callback)

      expect(callback).toHaveBeenCalledWith({
        success: true,
        error: null,
        detail: null,
        data: patchedTask,
      })
      requestSpy.mockRestore()
    })

    it('handles validation errors with callback', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 400,
          jsonData: { detail: [{ loc: ['timer'], msg: 'Invalid CRON expression', type: 'value_error' }] },
        }),
      )

      const callback = mock(() => {})
      const tasks = useTasks()

      await tasks.patchTask(1, { timer: 'invalid' }, callback)

      expect(callback).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          error: expect.any(String),
        }),
      )
      requestSpy.mockRestore()
    })
  })

  describe('deleteTask', () => {
    it('deletes a task successfully', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: mockTask,
        }),
      )

      const tasks = useTasks()
      const initialTotal = tasks.pagination.value.total

      const result = await tasks.deleteTask(mockTask.id!)

      expect(result).toBe(true)
      expect(tasks.pagination.value.total).toBe(Math.max(0, initialTotal - 1))
      requestSpy.mockRestore()
    })

    it('calls callback on delete success', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: mockTask,
        }),
      )

      const callback = mock(() => {})
      const tasks = useTasks()

      await tasks.deleteTask(1, callback)

      expect(callback).toHaveBeenCalledWith({
        success: true,
        error: null,
        detail: null,
        data: true,
      })
      requestSpy.mockRestore()
    })

    it('calls callback on delete error', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 404,
          jsonData: { error: 'Task not found' },
        }),
      )

      const callback = mock(() => {})
      const tasks = useTasks()

      await tasks.deleteTask(999, callback)

      expect(callback).toHaveBeenCalledWith(
        expect.objectContaining({
          success: false,
          error: expect.any(String),
          data: false,
        }),
      )
      requestSpy.mockRestore()
    })
  })

  describe('inspectTaskHandler', () => {
    it('inspects task handler successfully', async () => {
      const inspectResponse: TaskInspectResponse = {
        matched: true,
        handler: 'YoutubeHandler',
        supported: true,
        items: [
          {
            url: 'https://www.youtube.com/watch?v=test123',
            title: 'Test Video',
            archive_id: 'youtube test123',
            metadata: { published: '2024-01-01T00:00:00Z' },
          },
        ],
        metadata: {
          feed_url: 'https://www.youtube.com/feeds/videos.xml?channel_id=UCtest123',
          entry_count: 1,
        },
      }

      const fetchSpy = spyOn(globalThis, 'fetch')
      fetchSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: inspectResponse,
        }),
      )

      const tasks = useTasks()
      const result = await tasks.inspectTaskHandler({
        url: 'https://www.youtube.com/channel/UCtest123',
      })

      expect(result).toEqual(inspectResponse)
      expect(result?.matched).toBe(true)
      expect(result?.handler).toBe('YoutubeHandler')
      fetchSpy.mockRestore()
    })

    it('handles handler not supported', async () => {
      const inspectResponse: TaskInspectResponse = {
        matched: false,
        handler: null,
        supported: false,
        items: [],
        metadata: null,
      }

      const fetchSpy = spyOn(globalThis, 'fetch')
      fetchSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: inspectResponse,
        }),
      )

      const tasks = useTasks()
      const result = await tasks.inspectTaskHandler({
        url: 'https://unsupported.com',
      })

      expect(result?.supported).toBe(false)
      expect(result?.matched).toBe(false)
      fetchSpy.mockRestore()
    })

    it('handles inspect errors', async () => {
      const fetchSpy = spyOn(globalThis, 'fetch')
      fetchSpy.mockRejectedValueOnce(new Error('Network error'))

      const tasks = useTasks()
      const result = await tasks.inspectTaskHandler({
        url: 'invalid',
      })

      expect(result).toBeNull()
      expect(tasks.lastError.value).toBeTruthy()
      fetchSpy.mockRestore()
    })
  })

  describe('markTaskItems', () => {
    it('marks all items as downloaded successfully', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: { message: 'Marked 15 items' },
        }),
      )

      const tasks = useTasks()
      const result = await tasks.markTaskItems(1)

      expect(result).toBe('Marked 15 items')
      expect(tasks.lastError.value).toBeNull()
      requestSpy.mockRestore()
    })

    it('handles mark errors', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 404,
          jsonData: { error: 'Task not found' },
        }),
      )

      const tasks = useTasks()
      const result = await tasks.markTaskItems(999)

      expect(result).toBeNull()
      expect(tasks.lastError.value).toBeTruthy()
      requestSpy.mockRestore()
    })
  })

  describe('unmarkTaskItems', () => {
    it('unmarks items successfully', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: { message: 'Unmarked 10 items' },
        }),
      )

      const tasks = useTasks()
      const result = await tasks.unmarkTaskItems(1)

      expect(result).toBe('Unmarked 10 items')
      expect(tasks.lastError.value).toBeNull()
      requestSpy.mockRestore()
    })

    it('handles unmark errors', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 404,
          jsonData: { error: 'Task not found' },
        }),
      )

      const tasks = useTasks()
      const result = await tasks.unmarkTaskItems(999)

      expect(result).toBeNull()
      expect(tasks.lastError.value).toBeTruthy()
      requestSpy.mockRestore()
    })
  })

  describe('generateTaskMetadata', () => {
    it('generates metadata successfully', async () => {
      const metadataResponse: TaskMetadataResponse = {
        status: 'success',
        generated: ['tvshow.nfo', 'info.json', 'poster.jpg'],
      }

      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: metadataResponse,
        }),
      )

      const tasks = useTasks()
      const result = await tasks.generateTaskMetadata(1)

      expect(result).toEqual(metadataResponse)
      expect(result?.status).toBe('success')
      expect(tasks.lastError.value).toBeNull()
      requestSpy.mockRestore()
    })

    it('handles metadata generation errors', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 500,
          jsonData: { error: 'Failed to generate metadata' },
        }),
      )

      const tasks = useTasks()
      const result = await tasks.generateTaskMetadata(1)

      expect(result).toBeNull()
      expect(tasks.lastError.value).toBeTruthy()
      requestSpy.mockRestore()
    })
  })

  describe('error handling', () => {
    it('throws when throwInstead is true', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 500,
          jsonData: { error: 'Server error' },
        }),
      )

      const tasks = useTasks()
      tasks.throwInstead.value = true

      await expect(tasks.loadTasks()).rejects.toThrow()
      requestSpy.mockRestore()
    })

    it('clears error on clearError call', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 500,
          jsonData: { error: 'Server error' },
        }),
      )

      const tasks = useTasks()
      tasks.throwInstead.value = false

      await tasks.loadTasks()

      expect(tasks.lastError.value).toBeTruthy()

      tasks.clearError()

      expect(tasks.lastError.value).toBeNull()
      requestSpy.mockRestore()
    })

    it('parses API validation errors correctly', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 400,
          jsonData: {
            detail: [
              { loc: ['name'], msg: 'Field required', type: 'value_error.missing' },
              { loc: ['url'], msg: 'Invalid URL format', type: 'value_error.url' },
            ],
          },
        }),
      )

      const tasks = useTasks()
      tasks.clearError()
      
      const newTask = { ...mockTask, name: '', url: '' }
      delete (newTask as any).id

      const result = await tasks.createTask(newTask)

      expect(result).toBeNull()
      expect(tasks.lastError.value).toBeTruthy()
      expect(tasks.lastError.value).toContain('name: Field required')
      expect(tasks.lastError.value).toContain('url: Invalid URL format')
      requestSpy.mockRestore()
    })
  })

  describe('addInProgress state', () => {
    it('sets addInProgress during create operation', async () => {
      let inProgressDuringCall = false

      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockImplementation(async (_url, _options) => {
        const currentTasks = useTasks()
        inProgressDuringCall = currentTasks.addInProgress.value
        return createMockResponse({
          ok: true,
          status: 200,
          jsonData: mockTask,
        })
      })

      const tasks = useTasks()
      const newTask = { ...mockTask }
      delete (newTask as any).id

      await tasks.createTask(newTask)

      expect(inProgressDuringCall).toBe(true)
      expect(tasks.addInProgress.value).toBe(false)
      requestSpy.mockRestore()
    })

    it('resets addInProgress on error', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: false,
          status: 400,
          jsonData: { error: 'Bad request' },
        }),
      )

      const tasks = useTasks()
      const newTask = { ...mockTask }
      delete (newTask as any).id

      await tasks.createTask(newTask)

      expect(tasks.addInProgress.value).toBe(false)
      requestSpy.mockRestore()
    })
  })

  describe('isLoading state', () => {
    it('sets isLoading during loadTasks operation', async () => {
      let loadingDuringCall = false

      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockImplementation(async (_url, _options) => {
        const currentTasks = useTasks()
        loadingDuringCall = currentTasks.isLoading.value
        return createMockResponse({
          ok: true,
          status: 200,
          jsonData: {
            items: [mockTask],
            pagination: mockPagination,
          },
        })
      })

      const tasks = useTasks()
      await tasks.loadTasks()

      expect(loadingDuringCall).toBe(true)
      expect(tasks.isLoading.value).toBe(false)
      requestSpy.mockRestore()
    })
  })

  describe('task list updates', () => {
    it('updates existing task in list', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: {
            items: [mockTask],
            pagination: mockPagination,
          },
        }),
      )

      const tasks = useTasks()
      await tasks.loadTasks()

      expect(tasks.tasks.value).toHaveLength(1)
      expect(tasks.tasks.value[0].name).toBe('Test Task')

      const updatedTask = { ...mockTask, name: 'Updated Name' }
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: updatedTask,
        }),
      )

      await tasks.updateTask(1, updatedTask)

      expect(tasks.tasks.value[0].name).toBe('Updated Name')
      requestSpy.mockRestore()
    })

    it('removes deleted task from list', async () => {
      const requestSpy = spyOn(utils, 'request')
      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: {
            items: [mockTask],
            pagination: { ...mockPagination, total: 1 },
          },
        }),
      )

      const tasks = useTasks()
      await tasks.loadTasks()

      expect(tasks.tasks.value).toHaveLength(1)
      expect(tasks.pagination.value.total).toBe(1)

      requestSpy.mockResolvedValueOnce(
        createMockResponse({
          ok: true,
          status: 200,
          jsonData: mockTask,
        }),
      )

      await tasks.deleteTask(1)

      expect(tasks.tasks.value).toHaveLength(0)
      expect(tasks.pagination.value.total).toBe(0)
      requestSpy.mockRestore()
    })
  })
})
