/**
 * Unit tests for AddParams.upsert — create-or-update in a single add() call.
 *
 * Covers the live add() path and the addMany batch flag. Verifies that upsert
 * inserts on a fresh id, merges (not overwrites) into an existing id while
 * preserving createdAt and bumping _rev, is mutually exclusive with ifAbsent,
 * and is a plain insert when no id is supplied.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../../src/brainy'
import { createTestConfig } from '../../helpers/test-factory'

describe('AddParams.upsert', () => {
  let brain: Brainy

  beforeEach(async () => {
    brain = new Brainy(createTestConfig())
    await brain.init()
  })

  afterEach(async () => {
    await brain.close()
  })

  it('inserts a new entity when the id does not exist (_rev 1)', async () => {
    // 8.0 normalizes a natural string key to a stable canonical UUID; the entity is
    // still reachable by the natural key via get()'s id resolution.
    const id = await brain.add({
      id: 'customer-1',
      type: 'thing',
      data: 'Acme Corp',
      metadata: { tier: 'silver' },
      upsert: true
    })

    expect(typeof id).toBe('string')

    const entity = await brain.get('customer-1')
    expect(entity).not.toBeNull()
    expect(entity!.type).toBe('thing')
    expect(entity!.metadata.tier).toBe('silver')
    expect(entity!._rev).toBe(1)
  })

  it('merges into an existing entity, preserves createdAt, bumps _rev, applies changed data', async () => {
    // Seed the entity.
    await brain.add({
      id: 'invoice-9',
      type: 'thing',
      data: 'Original line items',
      metadata: { status: 'draft', amount: 100 }
    })

    const before = await brain.get('invoice-9')
    expect(before).not.toBeNull()
    const originalCreatedAt = before!.createdAt
    expect(before!._rev).toBe(1)

    // Upsert over the existing id with a partial metadata patch + new data.
    await brain.add({
      id: 'invoice-9',
      type: 'thing',
      data: 'Revised line items',
      metadata: { status: 'sent' },
      upsert: true
    })

    const after = await brain.get('invoice-9')
    expect(after).not.toBeNull()

    // Metadata MERGED, not replaced — the untouched 'amount' field survives.
    expect(after!.metadata.status).toBe('sent')
    expect(after!.metadata.amount).toBe(100)

    // Changed data applied.
    expect(after!.data).toBe('Revised line items')

    // _rev bumped, createdAt preserved.
    expect(after!._rev).toBe(2)
    expect(after!.createdAt).toBe(originalCreatedAt)
  })

  it('re-embeds when data changes on upsert', async () => {
    await brain.add({
      id: 'doc-vec',
      type: 'document',
      data: 'cats and dogs and small furry animals'
    })
    const before = await brain.get('doc-vec', { includeVectors: true })
    expect(before!.vector.length).toBeGreaterThan(0)

    await brain.add({
      id: 'doc-vec',
      type: 'document',
      data: 'quarterly financial revenue projections spreadsheet',
      upsert: true
    })
    const after = await brain.get('doc-vec', { includeVectors: true })
    expect(after!.vector.length).toBe(before!.vector.length)
    // A genuinely different document re-embeds to a different vector.
    expect(after!.vector).not.toEqual(before!.vector)
  })

  it('throws when both ifAbsent and upsert are true', async () => {
    await expect(
      brain.add({
        id: 'conflict-1',
        type: 'thing',
        data: 'x',
        ifAbsent: true,
        upsert: true
      })
    ).rejects.toThrow(/mutually exclusive/)
  })

  it('behaves like a plain insert when no id is supplied', async () => {
    const id = await brain.add({
      type: 'thing',
      data: 'no id supplied',
      metadata: { k: 'v' },
      upsert: true
    })

    expect(typeof id).toBe('string')
    const entity = await brain.get(id)
    expect(entity).not.toBeNull()
    expect(entity!._rev).toBe(1)
    expect(entity!.metadata.k).toBe('v')
  })

  it('merges an existing item via the addMany batch upsert flag', async () => {
    // Seed an item that the batch will later upsert into.
    await brain.add({
      id: 'order-1',
      type: 'thing',
      data: 'first',
      metadata: { region: 'west', count: 1 }
    })
    const seeded = await brain.get('order-1')
    const seededCreatedAt = seeded!.createdAt

    const result = await brain.addMany({
      upsert: true,
      items: [
        { id: 'order-1', type: 'thing', data: 'updated', metadata: { count: 2 } },
        { id: 'order-2', type: 'thing', data: 'brand new', metadata: { region: 'east' } }
      ]
    })

    // Both items resolved (returned ids are the canonical UUIDs the natural keys map to).
    expect(result.failed).toHaveLength(0)
    expect(result.successful).toHaveLength(2)

    // Existing item merged (region survived), _rev bumped, createdAt preserved.
    const merged = await brain.get('order-1')
    expect(merged!.metadata.region).toBe('west')
    expect(merged!.metadata.count).toBe(2)
    expect(merged!.data).toBe('updated')
    expect(merged!._rev).toBe(2)
    expect(merged!.createdAt).toBe(seededCreatedAt)

    // New item inserted fresh.
    const fresh = await brain.get('order-2')
    expect(fresh!._rev).toBe(1)
    expect(fresh!.metadata.region).toBe('east')
  })

  it('upserts inside a transact batch — existing id merges, new id inserts', async () => {
    await brain.add({
      id: 'acct-1',
      type: 'thing',
      data: 'seed',
      metadata: { plan: 'free', seats: 1 }
    })
    const seeded = await brain.get('acct-1')
    const seededCreatedAt = seeded!.createdAt

    await brain.transact([
      { op: 'add', id: 'acct-1', type: 'thing', data: 'seed', metadata: { plan: 'pro' }, upsert: true },
      { op: 'add', id: 'acct-2', type: 'thing', data: 'new account', metadata: { plan: 'free' }, upsert: true }
    ])

    const merged = await brain.get('acct-1')
    expect(merged!.metadata.plan).toBe('pro')
    expect(merged!.metadata.seats).toBe(1) // merged, not overwritten
    expect(merged!._rev).toBe(2)
    expect(merged!.createdAt).toBe(seededCreatedAt)

    const fresh = await brain.get('acct-2')
    expect(fresh!._rev).toBe(1)
    expect(fresh!.metadata.plan).toBe('free')
  })
})
