/**
 * Tests for js/services/directus/client.js
 */

import { test, asyncTest, describe, assertEquals, assertTrue, assertFalse, assertThrows, assertDeepEquals, assertNotEquals } from './test-runner.js'
import { client, DirectusError, setPersist, getPersist } from '../js/services/directus/client.js'

// ── DirectusError ──

describe('DirectusError', () => {
    test('creates error with correct properties', () => {
        const err = new DirectusError(404, 'Not found', { foo: 'bar' })
        assertEquals(err.status, 404)
        assertEquals(err.message, 'Not found')
        assertEquals(err.name, 'DirectusError')
        assertDeepEquals(err.data, { foo: 'bar' })
    })

    test('extends Error', () => {
        const err = new DirectusError(500, 'fail')
        assertTrue(err instanceof Error)
        assertTrue(err instanceof DirectusError)
    })

    test('defaults data to empty object', () => {
        const err = new DirectusError(500, 'fail')
        assertDeepEquals(err.data, {})
    })

    test('isAuthError returns true for 401', () => {
        assertTrue(new DirectusError(401, 'Unauthorized').isAuthError())
    })

    test('isAuthError returns true for 403', () => {
        assertTrue(new DirectusError(403, 'Forbidden').isAuthError())
    })

    test('isAuthError returns false for other codes', () => {
        assertFalse(new DirectusError(404, 'Not found').isAuthError())
        assertFalse(new DirectusError(500, 'Server error').isAuthError())
    })

    test('isNotFound returns true for 404', () => {
        assertTrue(new DirectusError(404, 'Not found').isNotFound())
    })

    test('isNotFound returns false for other codes', () => {
        assertFalse(new DirectusError(401, 'Unauthorized').isNotFound())
    })

    test('isValidationError returns true for 400', () => {
        assertTrue(new DirectusError(400, 'Bad request').isValidationError())
    })

    test('isValidationError returns false for other codes', () => {
        assertFalse(new DirectusError(404, 'Not found').isValidationError())
    })
})

// ── buildQueryString ──

describe('DirectusClient.buildQueryString', () => {
    test('builds simple key-value pairs', () => {
        const qs = client.buildQueryString({ limit: 10, offset: 5 })
        assertTrue(qs.includes('limit=10'))
        assertTrue(qs.includes('offset=5'))
    })

    test('joins array fields with comma', () => {
        const qs = client.buildQueryString({ fields: ['id', 'title', 'status'] })
        assertTrue(qs.includes('fields=id%2Ctitle%2Cstatus') || qs.includes('fields=id,title,status'))
    })

    test('joins array sort with comma', () => {
        const qs = client.buildQueryString({ sort: ['-date_created', 'title'] })
        assertTrue(qs.includes('sort='))
    })

    test('stringifies object values as JSON', () => {
        const filter = { status: { _eq: 'published' } }
        const qs = client.buildQueryString({ filter })
        assertTrue(qs.includes('filter='))
        assertTrue(qs.includes('published'))
    })

    test('skips null and undefined values', () => {
        const qs = client.buildQueryString({ a: null, b: undefined, c: 'value' })
        assertFalse(qs.includes('a='))
        assertFalse(qs.includes('b='))
        assertTrue(qs.includes('c=value'))
    })

    test('returns empty string for empty params', () => {
        assertEquals(client.buildQueryString({}), '')
    })
})

// ── Persist toggle ──

describe('setPersist / getPersist', () => {
    test('defaults to false after module load', () => {
        // Note: may have been set by loadTokens() — just verify type
        assertEquals(typeof getPersist(), 'boolean')
    })

    test('setPersist changes value', () => {
        const original = getPersist()
        setPersist(true)
        assertEquals(getPersist(), true)
        setPersist(false)
        assertEquals(getPersist(), false)
        setPersist(original)
    })
})

// ── Token management ──

describe('DirectusClient token management', () => {
    test('clearTokens resets all token state', () => {
        client.clearTokens()
        assertEquals(client.accessToken, null)
        assertEquals(client.refreshToken, null)
        assertEquals(client.tokenExpiry, null)
    })

    test('isAuthenticated returns false without token', () => {
        client.clearTokens()
        assertFalse(client.isAuthenticated())
    })

    test('saveTokens stores tokens and sets expiry', () => {
        client.saveTokens('test-access', 'test-refresh', 900000)
        assertEquals(client.accessToken, 'test-access')
        assertEquals(client.refreshToken, 'test-refresh')
        assertTrue(client.tokenExpiry > Date.now())

        // Cleanup
        client.clearTokens()
        sessionStorage.removeItem('kashilo_auth')
        localStorage.removeItem('kashilo_auth')
    })

    test('isAuthenticated returns true with valid token', () => {
        client.saveTokens('valid-token', 'refresh', 900000)
        assertTrue(client.isAuthenticated())

        client.clearTokens()
        sessionStorage.removeItem('kashilo_auth')
        localStorage.removeItem('kashilo_auth')
    })

    test('isAuthenticated returns false when token expired', () => {
        client.accessToken = 'expired'
        client.tokenExpiry = Date.now() - 10000
        assertFalse(client.isAuthenticated())

        client.clearTokens()
    })

    test('loadTokens restores from storage', () => {
        const authData = JSON.stringify({
            accessToken: 'stored-access',
            refreshToken: 'stored-refresh',
            expiry: Date.now() + 600000
        })
        sessionStorage.setItem('kashilo_auth', authData)

        client.loadTokens()
        assertEquals(client.accessToken, 'stored-access')
        assertEquals(client.refreshToken, 'stored-refresh')

        client.clearTokens()
        sessionStorage.removeItem('kashilo_auth')
    })

    test('loadTokens clears on invalid JSON', () => {
        sessionStorage.setItem('kashilo_auth', 'not-json')
        client.loadTokens()
        assertEquals(client.accessToken, null)

        sessionStorage.removeItem('kashilo_auth')
    })
})

// ── HTTP request with mocked fetch ──

describe('DirectusClient.request', () => {
    asyncTest('makes GET request with auth header', async () => {
        const originalFetch = window.fetch

        client.accessToken = 'mock-token'
        let capturedHeaders = null

        window.fetch = async (url, opts) => {
            capturedHeaders = opts.headers
            return {
                ok: true,
                status: 200,
                json: async () => ({ data: { id: 1 } })
            }
        }

        const result = await client.request('/items/test', { method: 'GET' })
        assertEquals(capturedHeaders['Authorization'], 'Bearer mock-token')
        assertEquals(result.data.id, 1)

        window.fetch = originalFetch
        client.clearTokens()
    })

    asyncTest('throws DirectusError on non-ok response', async () => {
        const originalFetch = window.fetch

        window.fetch = async () => ({
            ok: false,
            status: 404,
            json: async () => ({ errors: [{ message: 'Item not found' }] })
        })

        let caught = null
        try {
            await client.request('/items/missing')
        } catch (e) {
            caught = e
        }

        assertTrue(caught instanceof DirectusError)
        assertEquals(caught.status, 404)
        assertTrue(caught.isNotFound())

        window.fetch = originalFetch
    })

    asyncTest('throws DirectusError with status 0 on network error', async () => {
        const originalFetch = window.fetch

        window.fetch = async () => { throw new TypeError('Failed to fetch') }

        let caught = null
        try {
            await client.request('/items/test')
        } catch (e) {
            caught = e
        }

        assertTrue(caught instanceof DirectusError)
        assertEquals(caught.status, 0)
        assertEquals(caught.message, 'Network error')

        window.fetch = originalFetch
    })

    asyncTest('returns null for 204 No Content', async () => {
        const originalFetch = window.fetch

        window.fetch = async () => ({
            ok: true,
            status: 204
        })

        const result = await client.request('/items/test', { method: 'DELETE' })
        assertEquals(result, null)

        window.fetch = originalFetch
    })

    asyncTest('retries on 429 rate limit', async () => {
        const originalFetch = window.fetch
        let callCount = 0

        window.fetch = async () => {
            callCount++
            if (callCount === 1) {
                return {
                    ok: false,
                    status: 429,
                    headers: { get: () => '0' },
                    json: async () => ({})
                }
            }
            return {
                ok: true,
                status: 200,
                json: async () => ({ data: 'ok' })
            }
        }

        const result = await client.request('/items/test')
        assertEquals(callCount, 2)
        assertEquals(result.data, 'ok')

        window.fetch = originalFetch
    })

    asyncTest('does not retry 401 on auth endpoints', async () => {
        const originalFetch = window.fetch
        client.refreshToken = 'some-token'
        let callCount = 0

        window.fetch = async () => {
            callCount++
            return {
                ok: false,
                status: 401,
                json: async () => ({ errors: [{ message: 'Unauthorized' }] })
            }
        }

        let caught = null
        try {
            await client.request('/auth/login')
        } catch (e) {
            caught = e
        }

        assertTrue(caught instanceof DirectusError)
        assertEquals(caught.status, 401)
        assertEquals(callCount, 1)

        window.fetch = originalFetch
        client.clearTokens()
    })
})
