// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import '../../../../../test/react-setup'

// Mock the repo store
vi.mock('../../../../store/repos', () => ({
  useRepoStore: vi.fn().mockImplementation((selector: (s: { repos: unknown[] }) => unknown) =>
    selector({ repos: [] })
  ),
}))

import { useSourceControlData } from './useSourceControlData'

beforeEach(() => {
  vi.clearAllMocks()
})

describe('useSourceControlData', () => {
  const defaultProps = {
    directory: '/repos/project',
    gitStatus: [],
    syncStatus: { current: 'feature/test', tracking: 'origin/feature/test', ahead: 0, behind: 0, files: [] },
    scView: 'working' as const,
  }

  it('returns initial state values', () => {
    const { result } = renderHook(() => useSourceControlData(defaultProps))

    expect(result.current.commitMessage).toBe('')
    expect(result.current.isCommitting).toBe(false)
    expect(result.current.commitError).toBeNull()
    expect(result.current.isSyncing).toBe(false)
    expect(result.current.gitOpError).toBeNull()
    expect(result.current.branchChanges).toEqual([])
    expect(result.current.branchBaseName).toBe('main')
    expect(result.current.checksStatus).toBe('none')
  })

  it('sets checksStatus to none when no PR exists', async () => {
    vi.mocked(window.gh.prStatus).mockResolvedValue(null)
    vi.mocked(window.gh.hasWriteAccess).mockResolvedValue(false)

    const { result } = renderHook(() => useSourceControlData(defaultProps))

    await act(async () => {
      await new Promise(r => setTimeout(r, 10))
    })

    expect(result.current.checksStatus).toBe('none')
  })

  it('computes staged and unstaged files', () => {
    const gitStatus = [
      { path: 'src/index.ts', status: 'modified' as const, staged: true, indexStatus: 'M', workingDirStatus: ' ' },
      { path: 'src/app.ts', status: 'added' as const, staged: false, indexStatus: ' ', workingDirStatus: 'A' },
      { path: 'src/utils.ts', status: 'modified' as const, staged: true, indexStatus: 'M', workingDirStatus: ' ' },
    ]
    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, gitStatus })
    )

    expect(result.current.stagedFiles).toHaveLength(2)
    expect(result.current.unstagedFiles).toHaveLength(1)
    expect(result.current.stagedFiles[0].path).toBe('src/index.ts')
    expect(result.current.unstagedFiles[0].path).toBe('src/app.ts')
  })

  it('updates commitMessage via setCommitMessage', () => {
    const { result } = renderHook(() => useSourceControlData(defaultProps))

    act(() => {
      result.current.setCommitMessage('test message')
    })

    expect(result.current.commitMessage).toBe('test message')
  })

  it('updates isCommitting via setIsCommitting', () => {
    const { result } = renderHook(() => useSourceControlData(defaultProps))

    act(() => {
      result.current.setIsCommitting(true)
    })

    expect(result.current.isCommitting).toBe(true)
  })

  it('updates commitError via setCommitError', () => {
    const { result } = renderHook(() => useSourceControlData(defaultProps))

    act(() => {
      result.current.setCommitError('hook failed')
    })

    expect(result.current.commitError).toBe('hook failed')
  })

  it('updates gitOpError via setGitOpError', () => {
    const { result } = renderHook(() => useSourceControlData(defaultProps))

    act(() => {
      result.current.setGitOpError({ operation: 'Push', message: 'failed' })
    })

    expect(result.current.gitOpError).toEqual({ operation: 'Push', message: 'failed' })
  })

  it('fetches branch changes when scView is branch', async () => {
    vi.mocked(window.git.branchChanges).mockResolvedValue({
      files: [{ path: 'src/index.ts', status: 'modified' }],
      baseBranch: 'main',
      mergeBase: 'abc123',
    })

    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, scView: 'branch' })
    )

    // Wait for the async effect to resolve
    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(window.git.branchChanges).toHaveBeenCalledWith('/repos/project')
    expect(result.current.branchChanges).toEqual([{ path: 'src/index.ts', status: 'modified' }])
    expect(result.current.branchBaseName).toBe('main')
    expect(result.current.branchMergeBase).toBe('abc123')
  })

  it('fetches branch commits when scView is commits', async () => {
    vi.mocked(window.git.branchCommits).mockResolvedValue({
      commits: [
        { hash: 'abc123', shortHash: 'abc123', message: 'Fix', author: 'test', date: '2024-01-01' },
      ],
      baseBranch: 'main',
    })

    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, scView: 'commits' })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(window.git.branchCommits).toHaveBeenCalledWith('/repos/project')
    expect(result.current.branchCommits).toHaveLength(1)
    expect(result.current.branchCommits[0].hash).toBe('abc123')
  })

  it('handles branch changes fetch error', async () => {
    vi.mocked(window.git.branchChanges).mockRejectedValue(new Error('git error'))

    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, scView: 'branch' })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(result.current.branchChanges).toEqual([])
    expect(result.current.branchMergeBase).toBe('')
    expect(result.current.isBranchLoading).toBe(false)
  })

  it('handles branch commits fetch error', async () => {
    vi.mocked(window.git.branchCommits).mockRejectedValue(new Error('git error'))

    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, scView: 'commits' })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(result.current.branchCommits).toEqual([])
    expect(result.current.isCommitsLoading).toBe(false)
  })

  it('resets state when directory changes', () => {
    const { result, rerender } = renderHook(
      (props) => useSourceControlData(props),
      { initialProps: defaultProps }
    )

    act(() => {
      result.current.setCommitError('some error')
      result.current.setGitOpError({ operation: 'Push', message: 'failed' })
    })

    expect(result.current.commitError).toBe('some error')

    // Change directory
    rerender({ ...defaultProps, directory: '/repos/other-project' })

    expect(result.current.commitError).toBeNull()
    expect(result.current.gitOpError).toBeNull()
  })

  it('provides the gitStatus from props', () => {
    const gitStatus = [{ path: 'src/index.ts', status: 'modified' as const, staged: false, indexStatus: ' ', workingDirStatus: 'M' }]
    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, gitStatus })
    )

    expect(result.current.gitStatus).toEqual(gitStatus)
  })

  it('fetches PR status on mount', async () => {
    vi.mocked(window.gh.prStatus).mockResolvedValue({
      number: 42,
      title: 'Test PR',
      state: 'OPEN',
      url: 'https://github.com/test/pr/42',
      headRefName: '',
      baseRefName: '',
    })
    vi.mocked(window.gh.hasWriteAccess).mockResolvedValue(true)
    vi.mocked(window.git.headCommit).mockResolvedValue('abc123')

    const { result } = renderHook(() => useSourceControlData(defaultProps))

    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(window.gh.prStatus).toHaveBeenCalledWith('/repos/project')
    expect(result.current.prStatus).toEqual({
      number: 42,
      title: 'Test PR',
      state: 'OPEN',
      url: 'https://github.com/test/pr/42',
      headRefName: '',
      baseRefName: '',
    })
    expect(result.current.hasWriteAccess).toBe(true)
  })

  it('handles PR status fetch error gracefully', async () => {
    vi.mocked(window.gh.prStatus).mockRejectedValue(new Error('network'))

    const { result } = renderHook(() => useSourceControlData(defaultProps))

    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(result.current.prStatus).toBeNull()
    expect(result.current.hasWriteAccess).toBe(false)
  })

  it('calls onUpdatePrState when PR status changes', async () => {
    const onUpdatePrState = vi.fn()
    vi.mocked(window.gh.prStatus).mockResolvedValue({
      number: 42,
      title: 'Test PR',
      state: 'OPEN',
      url: 'https://github.com/test/pr/42',
      headRefName: '',
      baseRefName: '',
    })
    vi.mocked(window.gh.hasWriteAccess).mockResolvedValue(true)
    vi.mocked(window.git.headCommit).mockResolvedValue('abc123')

    renderHook(() => useSourceControlData({ ...defaultProps, onUpdatePrState }))

    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(onUpdatePrState).toHaveBeenCalledWith('OPEN', 42, 'https://github.com/test/pr/42')
  })

  it('calls onUpdatePrState with null when no PR', async () => {
    const onUpdatePrState = vi.fn()
    vi.mocked(window.gh.prStatus).mockResolvedValue(null)
    vi.mocked(window.gh.hasWriteAccess).mockResolvedValue(false)
    vi.mocked(window.git.headCommit).mockResolvedValue(null)

    renderHook(() => useSourceControlData({ ...defaultProps, onUpdatePrState }))

    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(onUpdatePrState).toHaveBeenCalledWith(null)
  })

  it('fetches checks status for open PRs', async () => {
    vi.mocked(window.gh.prStatus).mockResolvedValue({
      number: 42, title: 'Test', state: 'OPEN', url: 'url',
      headRefName: 'feature', baseRefName: 'main',
    })
    vi.mocked(window.gh.hasWriteAccess).mockResolvedValue(true)
    vi.mocked(window.gh.prChecksStatus).mockResolvedValue('passed')

    const { result } = renderHook(() =>
      useSourceControlData(defaultProps)
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 10))
    })

    expect(result.current.checksStatus).toBe('passed')
  })

  it('does not fetch branch data when scView is not branch', () => {
    renderHook(() =>
      useSourceControlData({ ...defaultProps, scView: 'working' })
    )

    expect(window.git.branchChanges).not.toHaveBeenCalled()
  })

  it('does not fetch commits data when scView is not commits', () => {
    renderHook(() =>
      useSourceControlData({ ...defaultProps, scView: 'working' })
    )

    expect(window.git.branchCommits).not.toHaveBeenCalled()
  })

  it('returns initial behind-main state', () => {
    const { result } = renderHook(() => useSourceControlData(defaultProps))
    expect(result.current.behindMainCount).toBe(0)
    // Fetch fires on mount in the working view; isFetchingBehindMain flips true
    // until isBehindMain resolves. Behaviour: count starts at 0 and is replaced
    // asynchronously.
    expect(result.current.agentMergeMessage).toBeNull()
  })

  it('fetches behind-main count when pushed with no changes', async () => {
    vi.mocked(window.git.isBehindMain).mockResolvedValue({ behind: 5, defaultBranch: 'main' })

    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, branchStatus: 'pushed', gitStatus: [] })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 10))
    })

    expect(result.current.behindMainCount).toBe(5)
  })

  it('fetches behind-main count when open with no changes', async () => {
    vi.mocked(window.git.isBehindMain).mockResolvedValue({ behind: 3, defaultBranch: 'main' })

    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, branchStatus: 'open', gitStatus: [] })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 10))
    })

    expect(result.current.behindMainCount).toBe(3)
  })

  it('fetches behind-main even when branch is in-progress', async () => {
    vi.mocked(window.git.isBehindMain).mockResolvedValue({ behind: 7, defaultBranch: 'main' })
    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, branchStatus: 'in-progress', gitStatus: [] })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 10))
    })

    expect(window.git.isBehindMain).toHaveBeenCalled()
    expect(result.current.behindMainCount).toBe(7)
  })

  it('fetches behind-main even when there are uncommitted changes', async () => {
    vi.mocked(window.git.isBehindMain).mockResolvedValue({ behind: 4, defaultBranch: 'main' })
    const gitStatus = [{ path: 'file.ts', status: 'modified' as const, staged: false, indexStatus: ' ', workingDirStatus: 'M' }]
    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, branchStatus: 'pushed', gitStatus })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 10))
    })

    expect(window.git.isBehindMain).toHaveBeenCalled()
    expect(result.current.behindMainCount).toBe(4)
  })

  it('handles behind-main fetch error gracefully', async () => {
    vi.mocked(window.git.isBehindMain).mockRejectedValue(new Error('network'))

    const { result } = renderHook(() =>
      useSourceControlData({ ...defaultProps, branchStatus: 'pushed', gitStatus: [] })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 10))
    })

    expect(result.current.behindMainCount).toBe(0)
  })

  it('skips PR fetch when no directory', async () => {
    renderHook(() =>
      useSourceControlData({ ...defaultProps, directory: undefined })
    )

    await act(async () => {
      await new Promise(r => setTimeout(r, 0))
    })

    expect(window.gh.prStatus).not.toHaveBeenCalled()
  })
})
