import React, { act } from 'react'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { createRoot } from 'react-dom/client'
import { ConnectionContext } from '../providers/ConnectionProvider'
import { useSignSolanaTransaction } from './useSignSolanaTransaction'
import type { UseSignSolanaTransactionReturn } from '@meshconnect/uwc-types'

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Minimal stub that satisfies ConnectionContextValue's connector shape. */
function makeConnector(
  signSolanaTransaction: (tx: Uint8Array) => Promise<Uint8Array>
) {
  return {
    signSolanaTransaction,
    // other connector methods are not exercised by this hook
    getSession: () => ({ isConnected: false }),
    isReady: () => true,
    subscribe: () => () => {},
    getWallets: () => [],
    getNetworks: () => []
  }
}

/**
 * Minimal context value that wraps a connector stub. Pass `{ activeAddress }`
 * as an object (not a bare arg) so an explicit `undefined` survives — a default
 * parameter would otherwise replace `undefined` with the connected address.
 */
function makeContextValue(
  connector: ReturnType<typeof makeConnector>,
  { activeAddress }: { activeAddress: string | undefined } = {
    activeAddress: 'SoLanaTestAddr1111111111111111111111111111'
  }
) {
  return {
    connector,
    session: {
      isConnected: activeAddress !== undefined,
      walletId: undefined,
      networkId: undefined,
      activeAddress
    },
    wallets: [],
    networks: [],
    isReady: true
  }
}

/**
 * Renders `useSignSolanaTransaction` inside a component that exposes its
 * return value via a captured ref. Returns the ref and cleanup helpers.
 *
 * Pattern mirrors ConnectionProvider.singleton.test.tsx: createRoot + act,
 * no @testing-library/react (not installed in this package).
 */
function renderHookInContext(
  contextValue: ReturnType<typeof makeContextValue>
) {
  const captured: { current: UseSignSolanaTransactionReturn | null } = {
    current: null
  }

  function Capture() {
    captured.current = useSignSolanaTransaction()
    return null
  }

  const container = document.createElement('div')
  document.body.appendChild(container)
  const root = createRoot(container)

  async function render() {
    await act(async () => {
      root.render(
        <ConnectionContext.Provider value={contextValue as never}>
          <Capture />
        </ConnectionContext.Provider>
      )
    })
  }

  async function unmount() {
    await act(async () => {
      root.unmount()
    })
    document.body.removeChild(container)
  }

  return { captured, render, unmount }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('useSignSolanaTransaction', () => {
  let container: HTMLDivElement
  let root: ReturnType<typeof createRoot>

  beforeEach(() => {
    container = document.createElement('div')
    document.body.appendChild(container)
    root = createRoot(container)
  })

  afterEach(async () => {
    await act(async () => {
      root.unmount()
    })
    if (document.body.contains(container)) {
      document.body.removeChild(container)
    }
  })

  // (a) throws when used outside ConnectionProvider
  it('throws when rendered outside ConnectionProvider', () => {
    function BareHook() {
      useSignSolanaTransaction()
      return null
    }

    expect(() => {
      // Synchronous render — React will throw during render, caught here.
      // Suppress the React error-boundary noise this logs on the way out.
      const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
      try {
        act(() => {
          root.render(<BareHook />)
        })
      } finally {
        errorSpy.mockRestore()
      }
    }).toThrowError(
      'useSignSolanaTransaction must be used within a ConnectionProvider'
    )
  })

  // (b) returns the Uint8Array from connector.signSolanaTransaction on success
  it('returns the signed bytes from connector on success', async () => {
    const signedBytes = new Uint8Array([1, 2, 3, 4])
    const mockSign = vi.fn().mockResolvedValue(signedBytes)
    const ctxValue = makeContextValue(makeConnector(mockSign))
    const { captured, render, unmount } = renderHookInContext(ctxValue)

    await render()
    expect(captured.current).not.toBeNull()

    const input = new Uint8Array([9, 8, 7])
    let result: Uint8Array | undefined

    await act(async () => {
      result = await captured.current!.signSolanaTransaction(input)
    })

    expect(mockSign).toHaveBeenCalledOnce()
    expect(mockSign).toHaveBeenCalledWith(input)
    expect(result).toBe(signedBytes)
    await unmount()
  })

  // (c) sets error state and re-throws when connector.signSolanaTransaction rejects
  it('sets error state and re-throws when connector rejects', async () => {
    const walletError = { code: 4001, message: 'User rejected' }
    const mockSign = vi.fn().mockRejectedValue(walletError)
    const ctxValue = makeContextValue(makeConnector(mockSign))
    const { captured, render, unmount } = renderHookInContext(ctxValue)

    await render()

    let thrownError: unknown
    await act(async () => {
      try {
        await captured.current!.signSolanaTransaction(new Uint8Array([1]))
      } catch (err) {
        thrownError = err
      }
    })

    expect(thrownError).toBe(walletError)
    expect(captured.current!.error).toBe(walletError)
    await unmount()
  })

  // (d) isLoading is true during the call and false after — success path
  it('sets isLoading=true during call and false after success', async () => {
    let resolveSign!: (v: Uint8Array) => void
    const pendingSign = new Promise<Uint8Array>(res => {
      resolveSign = res
    })
    const mockSign = vi.fn().mockReturnValue(pendingSign)
    const ctxValue = makeContextValue(makeConnector(mockSign))
    const { captured, render, unmount } = renderHookInContext(ctxValue)

    await render()

    // Start the async call but don't await it yet — capture loading mid-flight
    let callPromise: Promise<Uint8Array>
    await act(async () => {
      callPromise = captured.current!.signSolanaTransaction(new Uint8Array([5]))
    })

    // At this point the hook has flipped isLoading=true (setIsLoading runs before the await)
    expect(captured.current!.isLoading).toBe(true)

    // Settle the promise
    await act(async () => {
      resolveSign(new Uint8Array([99]))
      await callPromise!
    })

    expect(captured.current!.isLoading).toBe(false)
    await unmount()
  })

  // (d) isLoading is false after failure path
  it('sets isLoading=false after connector rejects', async () => {
    let rejectSign!: (err: unknown) => void
    const pendingSign = new Promise<Uint8Array>((_, rej) => {
      rejectSign = rej
    })
    const mockSign = vi.fn().mockReturnValue(pendingSign)
    const ctxValue = makeContextValue(makeConnector(mockSign))
    const { captured, render, unmount } = renderHookInContext(ctxValue)

    await render()

    let callPromise: Promise<Uint8Array>
    await act(async () => {
      callPromise = captured.current!.signSolanaTransaction(new Uint8Array([6]))
    })

    expect(captured.current!.isLoading).toBe(true)

    await act(async () => {
      rejectSign({ code: 4001, message: 'cancelled' })
      try {
        await callPromise!
      } catch {
        // expected
      }
    })

    expect(captured.current!.isLoading).toBe(false)
    await unmount()
  })

  // (e) throws "No wallet connected" before touching the connector when no active address
  it('throws "No wallet connected" when there is no active address', async () => {
    const mockSign = vi.fn()
    const ctxValue = makeContextValue(makeConnector(mockSign), {
      activeAddress: undefined
    })
    const { captured, render, unmount } = renderHookInContext(ctxValue)

    await render()

    let thrownError: unknown
    await act(async () => {
      try {
        await captured.current!.signSolanaTransaction(new Uint8Array([1]))
      } catch (err) {
        thrownError = err
      }
    })

    expect((thrownError as Error).message).toBe('No wallet connected')
    expect(mockSign).not.toHaveBeenCalled()
    await unmount()
  })

  // (f) rejects empty input before touching the connector
  it('throws "Transaction bytes are required" for empty bytes', async () => {
    const mockSign = vi.fn()
    const ctxValue = makeContextValue(makeConnector(mockSign))
    const { captured, render, unmount } = renderHookInContext(ctxValue)

    await render()

    let thrownError: unknown
    await act(async () => {
      try {
        await captured.current!.signSolanaTransaction(new Uint8Array(0))
      } catch (err) {
        thrownError = err
      }
    })

    expect((thrownError as Error).message).toBe(
      'Transaction bytes are required'
    )
    expect(mockSign).not.toHaveBeenCalled()
    await unmount()
  })
})
