/**
 * IntegrationService — credential cache regression tests
 *
 * Regression guard for the context.store plumbing bug fixed in commit 75205cd8e:
 * IntegrationService.call() reads context.store.get(cacheKey) for per-execution
 * credential caching. Before the fix, dispatchIntegrationTool never threaded `store`
 * into the context, causing a V8 TypeError on every integration tool call.
 *
 * These tests exercise the real IntegrationService.call() code path — not a mock —
 * so any future regression that removes context.store from the dispatch path will
 * cause a real crash here, not a silent type error.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { IntegrationService } from './service'
import type { BaseIntegrationAdapter } from './base-integration-adapter'
import type { ExecutionContext } from '../../base/types'

// ---------------------------------------------------------------------------
// Mock the registry that IntegrationService.call() uses internally
// ---------------------------------------------------------------------------

const mockGetCredential = vi.fn()

vi.mock('../registry', () => ({
  getToolServices: () => ({
    credentialsService: {
      getCredential: mockGetCredential
    }
  })
}))

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

/** Minimal stub adapter — always validates, always succeeds */
function makeStubAdapter(name: string): BaseIntegrationAdapter {
  return {
    name,
    validateCredentials: () => true,
    call: vi.fn().mockResolvedValue({ stubResult: true })
  }
}

/** Minimal ExecutionContext with a real Map for store */
function makeContext(store: Map<string, unknown> = new Map()): ExecutionContext {
  return {
    executionId: 'exec-test-001',
    organizationId: 'org-test-001',
    organizationName: 'Test Org',
    resourceId: 'resource-test-001',
    executionDepth: 0,
    logger: {
      debug: () => {},
      info: () => {},
      warn: () => {},
      error: () => {}
    },
    store
  }
}

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

describe('IntegrationService — credential cache (context.store)', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  describe('call() with a valid store Map', () => {
    it('does not throw when context.store is a real Map (regression: was crashing with undefined.get)', async () => {
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))
      const credentials = { apiKey: 'sk_test_123' }

      mockGetCredential.mockResolvedValue(credentials)

      const context = makeContext()

      // This call must NOT throw "Cannot read properties of undefined (reading 'get')"
      const result = await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', context)

      expect(result).toEqual({ stubResult: true })
    })

    it('fetches credentials from credentialsService on first call', async () => {
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))
      const credentials = { apiKey: 'sk_test_123' }

      mockGetCredential.mockResolvedValue(credentials)

      await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', makeContext())

      expect(mockGetCredential).toHaveBeenCalledOnce()
      expect(mockGetCredential).toHaveBeenCalledWith('org-test-001', 'stripe-cred')
    })

    it('hits the cache and skips credentialsService on second call with the same store', async () => {
      // This is the core regression: the second call should reuse the cached credential
      // (store.get returns the value set by the first call) and NOT call credentialsService again.
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))
      const credentials = { apiKey: 'sk_test_123' }

      mockGetCredential.mockResolvedValue(credentials)

      // Use a SHARED store Map across both calls — as worker-executor does
      const sharedStore = new Map<string, unknown>()
      const ctx = makeContext(sharedStore)

      await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', ctx)
      await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', ctx)

      // credentialsService should only have been called once — second call hit the cache
      expect(mockGetCredential).toHaveBeenCalledOnce()
    })

    it('fetches credentials again when a fresh store is used (different execution)', async () => {
      // Two distinct executions each own their own store Map.
      // The credential cache must NOT leak across executions.
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))
      const credentials = { apiKey: 'sk_test_123' }

      mockGetCredential.mockResolvedValue(credentials)

      // Execution A — fresh store
      await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', makeContext(new Map()))
      // Execution B — different fresh store
      await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', makeContext(new Map()))

      // Each execution must independently fetch the credential
      expect(mockGetCredential).toHaveBeenCalledTimes(2)
    })

    it('writes the credential to store.set after fetching so subsequent calls can retrieve it', async () => {
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))
      const credentials = { apiKey: 'sk_test_456' }

      mockGetCredential.mockResolvedValue(credentials)

      const store = new Map<string, unknown>()
      await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', makeContext(store))

      // The credential must have been written into the store under the expected cache key
      const cacheKey = '__cred_cache__:stripe-cred'
      expect(store.has(cacheKey)).toBe(true)
      expect(store.get(cacheKey)).toEqual(credentials)
    })
  })

  describe('call() error conditions', () => {
    it('throws with credentials_missing when credentialsService returns null', async () => {
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))

      mockGetCredential.mockResolvedValue(null)

      await expect(service.call('stripe', 'listPaymentLinks', {}, 'missing-cred', makeContext())).rejects.toMatchObject(
        {
          errorType: 'credentials_missing'
        }
      )
    })

    it('throws with adapter_not_found when integration is not registered', async () => {
      const service = new IntegrationService(new Map())

      await expect(service.call('nonexistent', 'someMethod', {}, 'some-cred', makeContext())).rejects.toMatchObject({
        errorType: 'adapter_not_found'
      })
    })

    it('throws validation_error when context is undefined', async () => {
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))

      await expect(service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', undefined)).rejects.toMatchObject({
        errorType: 'validation_error'
      })
    })

    it('treats context.store as opportunistic — missing store falls through to credential fetch instead of crashing', async () => {
      // 2026-04-25 regression guard: the original bug crashed with
      //   "Cannot read properties of undefined (reading 'get')"
      // because context.store.get(cacheKey) had no optional chaining. After the
      // cache-as-contract-leak fix, the cache is treated as a perf optimization,
      // not a load-bearing contract. A missing store must therefore fall through
      // to the credentialsService fetch path and succeed.
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))
      const credentials = { apiKey: 'sk_test_123' }

      mockGetCredential.mockResolvedValue(credentials)

      const contextWithoutStore = {
        executionId: 'exec-broken',
        organizationId: 'org-test-001',
        organizationName: 'Test Org',
        resourceId: 'resource-broken',
        executionDepth: 0,
        logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }
        // store is intentionally absent
      } as unknown as ExecutionContext

      const result = await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', contextWithoutStore)

      expect(result).toEqual({ stubResult: true })
      // Credentials must have been fetched once (no cache hit, no cache write).
      expect(mockGetCredential).toHaveBeenCalledOnce()
      expect(mockGetCredential).toHaveBeenCalledWith('org-test-001', 'stripe-cred')
    })

    it('handles a null context.store the same way as undefined (opportunistic fallthrough)', async () => {
      const adapter = makeStubAdapter('stripe')
      const service = new IntegrationService(new Map([['stripe', adapter]]))
      mockGetCredential.mockResolvedValue({ apiKey: 'sk_test_123' })

      const contextNullStore = {
        executionId: 'exec-null',
        organizationId: 'org-test-001',
        organizationName: 'Test Org',
        resourceId: 'resource-null',
        executionDepth: 0,
        logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
        store: null
      } as unknown as ExecutionContext

      await expect(service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', contextNullStore)).resolves.toEqual({
        stubResult: true
      })
    })
  })
})
