/**
 * Cross-cutting integration tests for mizan
 *
 * Tests error paths and protocol correctness across HTTP, Forms, and WebSocket.
 * Requires a running backend: docker-compose up
 *
 * Run with: RUN_INTEGRATION_TESTS=true npm run test
 */

import { renderHook, act } from '@testing-library/react'
import { ReactNode } from 'react'
import { describeIntegration, BACKEND_URL, WS_URL } from '../testing'
import { MizanProvider, useMizan } from '../context'
import { MizanError } from '../errors'
import { ChannelConnection } from '../channels/connection'
import { RPCError } from '../channels/connection'

function Wrapper({ children }: { children: ReactNode }) {
    return (
        <MizanProvider baseUrl={`${BACKEND_URL}/api/mizan`} autoConnect={false}>
            {children}
        </MizanProvider>
    )
}

// Helper to get call function
function useCall() {
    const { call } = useMizan()
    return call
}

// Helper to wait for a ChannelConnection to reach 'connected' status
function waitForConnected(connection: ChannelConnection): Promise<void> {
    return new Promise((resolve) => {
        if (connection.status === 'connected') { resolve(); return }
        const unsub = connection.onStatusChange((status) => {
            if (status === 'connected') { unsub(); resolve() }
        })
    })
}

// ============================================================================
// Group 1: Executor framework validation
// ============================================================================

describeIntegration('Executor framework validation', () => {
    it('should return VALIDATION_ERROR with field details for wrong input types', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('add', { a: 'hello', b: 'world' })
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('VALIDATION_ERROR')
        expect(error!.isValidationError()).toBe(true)
        const fieldErrors = error!.getFieldErrors()
        expect(fieldErrors).not.toBeNull()
        expect(Object.keys(fieldErrors!).length).toBeGreaterThan(0)
    })

    it('should return NOT_FOUND for non-existent function', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('this_function_does_not_exist', {})
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('NOT_FOUND')
    })

    it('should return FORBIDDEN for auth-required function when anonymous', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('whoami', {})
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.isAuthError()).toBe(true)
    })

    it('should return VALIDATION_ERROR with specific field for missing required input', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('echo', {})
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('VALIDATION_ERROR')
        const fieldErrors = error!.getFieldErrors()
        expect(fieldErrors).not.toBeNull()
        expect(fieldErrors!).toHaveProperty('text')
    })
})

// ============================================================================
// Group 2: Form framework validation
// ============================================================================

describeIntegration('Form framework validation', () => {
    it('should return field metadata with types and required flags', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('login.schema', { data: {} })
        })

        expect(response).toHaveProperty('fields')

        // Each field should have name, label, type, required, widget
        const fields = response.fields
        for (const fieldKey of Object.keys(fields)) {
            const field = fields[fieldKey]
            expect(field).toHaveProperty('name')
            expect(field).toHaveProperty('label')
            expect(field).toHaveProperty('type')
            expect(field).toHaveProperty('required')
            expect(field).toHaveProperty('widget')
        }

        // login field should be required
        expect(fields.login.required).toBe(true)

        // password field widget should contain 'password'
        expect(fields.password.widget.toLowerCase()).toContain('password')
    })

    it('should return field-level errors for empty form validation', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('login.validate', { data: {} })
        })

        expect(response.valid).toBe(false)
        expect(response.errors).toBeInstanceOf(Array)
        expect(response.errors.length).toBeGreaterThan(0)
    })

    it('should return form-level error for wrong login credentials', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('login.submit', {
                login: 'wrong@example.com',
                password: 'wrongpass',
            })
        })

        expect(response.success).toBe(false)
        expect(JSON.stringify(response.errors)).toContain('Invalid login credentials')
    })
})

// ============================================================================
// Group 3: WebSocket framework validation
// ============================================================================

describeIntegration('WebSocket framework validation', () => {
    let connection: ChannelConnection

    beforeEach(async () => {
        connection = new ChannelConnection({ url: WS_URL, reconnect: false })
        connection.connect()

        // Wait for connected status
        await new Promise<void>((resolve) => {
            const unsub = connection.onStatusChange((status) => {
                if (status === 'connected') {
                    unsub()
                    resolve()
                }
            })
            if (connection.status === 'connected') {
                unsub()
                resolve()
            }
        })
    })

    afterEach(() => {
        connection.disconnect()
    })

    it('should deliver messages back through channel subscription', async () => {
        // Subscribe to chat channel and wait for confirmation
        await new Promise<void>((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error('Subscribe timeout')), 5000)
            const unsub = connection.onMessage((msg) => {
                if ('subscribed' in msg && msg.channel === 'chat') {
                    clearTimeout(timeout)
                    unsub()
                    resolve()
                }
            })
            connection.send({
                action: 'subscribe',
                channel: 'chat',
                params: { room: 'integration-test' },
            })
        })

        // Listen for the echoed message
        const messagePromise = new Promise<any>((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error('Message timeout')), 5000)
            const unsub = connection.onMessage((msg) => {
                if ('data' in msg) {
                    clearTimeout(timeout)
                    unsub()
                    resolve(msg)
                }
            })
        })

        // Send a message
        connection.send({
            action: 'message',
            channel: 'chat',
            params: { room: 'integration-test' },
            data: { text: 'hello from integration test' },
        })

        const received = await messagePromise
        expect(received.data).toHaveProperty('text')
        expect(received.data.text).toBe('hello from integration test')
    })

    it('should return error for unknown channel subscription', async () => {
        const errorPromise = new Promise<any>((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error('Error response timeout')), 5000)
            const unsub = connection.onMessage((msg) => {
                if ('error' in msg) {
                    clearTimeout(timeout)
                    unsub()
                    resolve(msg)
                }
            })
        })

        connection.send({
            action: 'subscribe',
            channel: 'nonexistent_channel',
            params: {},
        })

        const errorMsg = await errorPromise
        expect(errorMsg.error.toLowerCase()).toContain('unknown channel')
    })

    it('should reject HTTP-only function via WebSocket RPC', async () => {
        let rpcError: RPCError | null = null
        try {
            await connection.rpc('http_only_echo', { text: 'test' })
        } catch (e) {
            rpcError = e as RPCError
        }

        expect(rpcError).toBeInstanceOf(RPCError)
    })

    it('should return NOT_FOUND for non-existent RPC function', async () => {
        let rpcError: RPCError | null = null
        try {
            await connection.rpc('does_not_exist', {})
        } catch (e) {
            rpcError = e as RPCError
        }

        expect(rpcError).toBeInstanceOf(RPCError)
        expect(rpcError!.code).toBe('NOT_FOUND')
    })

    it('should return VALIDATION_ERROR for wrong RPC input types', async () => {
        let rpcError: RPCError | null = null
        try {
            await connection.rpc('add', { a: 'not_number', b: 'also_not' })
        } catch (e) {
            rpcError = e as RPCError
        }

        expect(rpcError).toBeInstanceOf(RPCError)
        expect(rpcError!.code).toBe('VALIDATION_ERROR')
    })
})

// ============================================================================
// Group 4: HTTP happy path
// ============================================================================

describeIntegration('HTTP happy path', () => {
    it('should call echo and receive echoed text', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('echo', { text: 'hello world' })
        })

        expect(response).toHaveProperty('message')
        expect(response.message).toContain('hello world')
    })

    it('should call add and receive correct sum', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('add', { a: 17, b: 25 })
        })

        expect(response).toEqual({ result: 42 })
    })

    it('should call class-based ServerFunction (multiply)', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('multiply', { x: 7, y: 6 })
        })

        expect(response).toEqual({ product: 42 })
    })
})

// ============================================================================
// Group 5: Auth variations
// ============================================================================

describeIntegration('Auth variations', () => {
    it('should reject staff_only for anonymous with UNAUTHORIZED', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('staff_only', {})
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('UNAUTHORIZED')
        expect(error!.isAuthError()).toBe(true)
    })

    it('should reject superuser_only for anonymous with UNAUTHORIZED', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('superuser_only', {})
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('UNAUTHORIZED')
        expect(error!.isAuthError()).toBe(true)
    })

    it('should reject verified_only for anonymous (callable auth)', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('verified_only', {})
            } catch (e) {
                error = e as MizanError
            }
        })

        // Callable auth returns False for anonymous, which maps to FORBIDDEN
        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('FORBIDDEN')
        expect(error!.isAuthError()).toBe(true)
    })
})

// ============================================================================
// Group 6: Context functions
// ============================================================================

describeIntegration('Context functions', () => {
    it('should call global context current_user and get anonymous response', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('current_user', {})
        })

        expect(response).toHaveProperty('authenticated', false)
        expect(response).toHaveProperty('email', '')
        expect(response).toHaveProperty('is_staff', false)
    })

    it('should call local context greet with name parameter', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('greet', { name: 'World' })
        })

        expect(response).toHaveProperty('greeting', 'Hello, World!')
    })
})

// ============================================================================
// Group 7: Error code coverage
// ============================================================================

describeIntegration('Error code coverage', () => {
    it('should return NOT_IMPLEMENTED for NotImplementedError', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('not_implemented_fn', {})
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('NOT_IMPLEMENTED')
    })

    it('should return INTERNAL_ERROR for unhandled RuntimeError', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('buggy_fn', {})
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('INTERNAL_ERROR')
    })

    it('should return FORBIDDEN for PermissionError with wrong secret', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let error: MizanError | null = null
        await act(async () => {
            try {
                await result.current('permission_check_fn', { secret: 'wrong' })
            } catch (e) {
                error = e as MizanError
            }
        })

        expect(error).toBeInstanceOf(MizanError)
        expect(error!.code).toBe('FORBIDDEN')
        expect(error!.isAuthError()).toBe(true)
    })

    it('should succeed with correct secret for permission_check_fn', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('permission_check_fn', { secret: 'open-sesame' })
        })

        expect(response).toEqual({ message: 'access granted' })
    })

    it('should return BAD_REQUEST for invalid JSON body', async () => {
        const response = await fetch(`${BACKEND_URL}/api/mizan/call/`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            credentials: 'include',
            body: '{not valid json}',
        })
        const data = await response.json()
        expect(data.error).toBe(true)
        expect(data.code).toBe('BAD_REQUEST')
    })

    it('should return BAD_REQUEST for missing fn field', async () => {
        const response = await fetch(`${BACKEND_URL}/api/mizan/call/`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            credentials: 'include',
            body: JSON.stringify({ args: {} }),
        })
        const data = await response.json()
        expect(data.error).toBe(true)
        expect(data.code).toBe('BAD_REQUEST')
    })
})

// ============================================================================
// Group 8: mizanFormMixin integration
// ============================================================================

describeIntegration('mizanFormMixin integration', () => {
    it('should return schema with title, subtitle, and submit_label from mizanFormMeta', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('contact.schema', { data: {} })
        })

        expect(response).toHaveProperty('fields')
        const fields = response.fields

        // Contact form should have name, email, and message fields
        expect(fields).toHaveProperty('name')
        expect(fields).toHaveProperty('email')
        expect(fields).toHaveProperty('message')

        // Meta should include title, subtitle, and submit_label
        expect(response).toHaveProperty('meta')
        expect(response.meta.title).toBe('Contact Us')
        expect(response.meta).toHaveProperty('subtitle')
        expect(response.meta.submit_label).toBe('Send Message')
    })

    it('should return form meta with live_validation and live_form_errors', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('contact.schema', { data: {} })
        })

        expect(response).toHaveProperty('meta')
        expect(response.meta.live_validation).toBe(true)
        expect(response.meta.live_form_errors).toBe(false)
    })

    it('should validate contact form and return field errors for missing fields', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('contact.validate', { data: {} })
        })

        expect(response.valid).toBe(false)
        expect(response.errors).toBeInstanceOf(Array)
        expect(response.errors.length).toBeGreaterThan(0)

        // Should have errors for name, email, and message
        const errorFieldNames = response.errors.map((e: any) => e.field)
        expect(errorFieldNames).toContain('name')
        expect(errorFieldNames).toContain('email')
        expect(errorFieldNames).toContain('message')
    })

    it('should submit contact form successfully and get on_submit_success data', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('contact.submit', {
                name: 'Test',
                email: 'test@test.com',
                message: 'Hello',
            })
        })

        expect(response.success).toBe(true)
        expect(response.data).toHaveProperty('received', true)
        expect(response.data).toHaveProperty('from', 'test@test.com')
    })
})

// ============================================================================
// Group 9: Formset integration
// ============================================================================

describeIntegration('Formset integration', () => {
    it('should return formset schema for item form', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('item.formset.schema', { forms: [] })
        })

        expect(response).toHaveProperty('min_num')
        expect(response).toHaveProperty('max_num')
    })

    it('should validate formset with invalid data', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('item.formset.validate', {
                forms: [{ label: '', quantity: 0 }],
            })
        })

        // Should have validation errors for the invalid form data
        expect(response).toHaveProperty('errors')
    })

    it('should submit formset with valid data', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        let response: any = null
        await act(async () => {
            response = await result.current('item.formset.submit', {
                forms: [{ label: 'Widget', quantity: 5 }],
            })
        })

        expect(response).toHaveProperty('success', true)
    })
})

// ============================================================================
// Group 10: Channel authorization
// ============================================================================

describeIntegration('Channel authorization', () => {
    let connection: ChannelConnection

    beforeEach(async () => {
        connection = new ChannelConnection({ url: WS_URL, reconnect: false })
        connection.connect()
        await waitForConnected(connection)
    })

    afterEach(() => {
        connection.disconnect()
    })

    it('should reject subscription to private channel when anonymous', async () => {
        const msgPromise = new Promise<any>((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error('Auth rejection timeout')), 5000)
            connection.onMessage((msg) => {
                if ('error' in msg) {
                    clearTimeout(timeout)
                    resolve(msg)
                }
            })
        })

        connection.send({ action: 'subscribe', channel: 'private' })

        const msg = await msgPromise
        expect(msg.error).toContain('Not authorized')
    })

    it('should successfully unsubscribe from a channel', async () => {
        // First subscribe to chat
        await new Promise<void>((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error('Subscribe timeout')), 5000)
            const unsub = connection.onMessage((msg) => {
                if ('subscribed' in msg && msg.channel === 'chat') {
                    clearTimeout(timeout)
                    unsub()
                    resolve()
                }
            })
            connection.send({
                action: 'subscribe',
                channel: 'chat',
                params: { room: 'unsub-test' },
            })
        })

        // Now unsubscribe
        const unsubPromise = new Promise<any>((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error('Unsubscribe timeout')), 5000)
            const unsub = connection.onMessage((msg) => {
                if ('unsubscribed' in msg && msg.channel === 'chat') {
                    clearTimeout(timeout)
                    unsub()
                    resolve(msg)
                }
            })
        })

        connection.send({
            action: 'unsubscribe',
            channel: 'chat',
            params: { room: 'unsub-test' },
        })

        const unsubMsg = await unsubPromise
        expect(unsubMsg).toHaveProperty('unsubscribed', true)
        expect(unsubMsg).toHaveProperty('channel', 'chat')
    })
})

// ============================================================================
// Group 11: WebSocket RPC happy path
// ============================================================================

describeIntegration('WebSocket RPC happy path', () => {
    let connection: ChannelConnection

    beforeEach(async () => {
        connection = new ChannelConnection({ url: WS_URL, reconnect: false })
        connection.connect()
        await waitForConnected(connection)
    })

    afterEach(() => {
        connection.disconnect()
    })

    it('should call echo via RPC and get correct response', async () => {
        const response = await connection.rpc<{ text: string }, { message: string }>(
            'echo',
            { text: 'ws rpc echo' }
        )

        expect(response).toHaveProperty('message')
        expect(response.message).toContain('ws rpc echo')
    })

    it('should call add via RPC and get correct sum', async () => {
        const response = await connection.rpc<{ a: number; b: number }, { result: number }>(
            'add',
            { a: 100, b: 200 }
        )

        expect(response).toEqual({ result: 300 })
    })

    it('should reject multiply via RPC if not websocket-enabled', async () => {
        // multiply uses @register_as which may not set websocket=True
        // If it's HTTP-only, RPC should fail; if it supports WS, it should succeed
        let response: any = null
        let rpcError: RPCError | null = null
        try {
            response = await connection.rpc<{ x: number; y: number }, { product: number }>(
                'multiply',
                { x: 7, y: 6 }
            )
        } catch (e) {
            rpcError = e as RPCError
        }

        // Either it succeeds with the correct product, or it fails because it's HTTP-only
        if (rpcError) {
            expect(rpcError).toBeInstanceOf(RPCError)
        } else {
            expect(response).toEqual({ product: 42 })
        }
    })

    it('should reject ws_whoami via RPC when anonymous', async () => {
        let rpcError: RPCError | null = null
        try {
            await connection.rpc('ws_whoami', {})
        } catch (e) {
            rpcError = e as RPCError
        }

        expect(rpcError).toBeInstanceOf(RPCError)
        expect(rpcError!.code).toBe('UNAUTHORIZED')
    })
})

// ============================================================================
// Group 12: Successful form submit flow
// ============================================================================

describeIntegration('Form submit success flow', () => {
    it('should sign up a new user via signup form', async () => {
        const { result } = renderHook(() => useCall(), { wrapper: Wrapper })

        // Use a unique email per run to avoid duplicate-user errors
        const uniqueEmail = `newuser+${Date.now()}@example.com`

        let response: any = null
        await act(async () => {
            response = await result.current('signup.submit', {
                email: uniqueEmail,
                password1: 'testpass123',
            })
        })

        expect(response).toHaveProperty('success', true)
        expect(response).toHaveProperty('data')
        expect(response.data).toHaveProperty('user_id')
        expect(typeof response.data.user_id).toBe('number')
    })
})
