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 { useSignTypedData } from './useSignTypedData'
import type {
  EIP712TypedData,
  UseSignTypedDataReturn
} from '@meshconnect/uwc-types'

// ---------------------------------------------------------------------------
// Fixtures + helpers
// ---------------------------------------------------------------------------

const USDC_TYPED_DATA: EIP712TypedData = {
  domain: {
    name: 'USD Coin',
    version: '2',
    chainId: 1,
    verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
  },
  types: {
    TransferWithAuthorization: [
      { name: 'from', type: 'address' },
      { name: 'to', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'validAfter', type: 'uint256' },
      { name: 'validBefore', type: 'uint256' },
      { name: 'nonce', type: 'bytes32' }
    ]
  },
  primaryType: 'TransferWithAuthorization',
  message: {
    from: '0xabc',
    to: '0xdef',
    value: '1000000',
    validAfter: 0,
    validBefore: 9999999999,
    nonce: '0xdeadbeef'
  }
}

/** Minimal stub that satisfies ConnectionContextValue's connector shape. */
function makeConnector(
  signTypedData: (typedData: EIP712TypedData) => Promise<string>
) {
  return {
    signTypedData,
    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.
 */
function makeContextValue(
  connector: ReturnType<typeof makeConnector>,
  { activeAddress }: { activeAddress: string | undefined } = {
    activeAddress: '0xUserAddress'
  }
) {
  return {
    connector,
    session: {
      isConnected: activeAddress !== undefined,
      walletId: undefined,
      networkId: undefined,
      activeAddress
    },
    wallets: [],
    networks: [],
    isReady: true
  }
}

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

  function Capture() {
    captured.current = useSignTypedData()
    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('useSignTypedData', () => {
  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)
    }
  })

  it('throws when rendered outside ConnectionProvider', () => {
    function BareHook() {
      useSignTypedData()
      return null
    }

    expect(() => {
      const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
      try {
        act(() => {
          root.render(<BareHook />)
        })
      } finally {
        errorSpy.mockRestore()
      }
    }).toThrowError('useSignTypedData must be used within a ConnectionProvider')
  })

  it('returns the signature from connector.signTypedData on success', async () => {
    const mockSign = vi.fn().mockResolvedValue('0xtypedsig')
    const ctxValue = makeContextValue(makeConnector(mockSign))
    const { captured, render, unmount } = renderHookInContext(ctxValue)

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

    let result: string | undefined
    await act(async () => {
      result = await captured.current!.signTypedData(USDC_TYPED_DATA)
    })

    expect(mockSign).toHaveBeenCalledOnce()
    expect(mockSign).toHaveBeenCalledWith(USDC_TYPED_DATA)
    expect(result).toBe('0xtypedsig')
    expect(captured.current!.signature).toBe('0xtypedsig')
    await unmount()
  })

  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!.signTypedData(USDC_TYPED_DATA)
      } catch (err) {
        thrownError = err
      }
    })

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

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

    await render()

    let callPromise: Promise<string>
    await act(async () => {
      callPromise = captured.current!.signTypedData(USDC_TYPED_DATA)
    })

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

    await act(async () => {
      resolveSign('0xsig')
      await callPromise!
    })

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

  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!.signTypedData(USDC_TYPED_DATA)
      } catch (err) {
        thrownError = err
      }
    })

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