import { test } from '@japa/runner'

import { DataDomain } from '../../../../src/modeling/DataDomain.js'
import { DomainModel } from '../../../../src/modeling/DomainModel.js'
import { DomainEntity } from '../../../../src/modeling/DomainEntity.js'
import { DomainProperty } from '../../../../src/modeling/DomainProperty.js'
import { CsvImporter, type ParseResult } from '../../../../src/modeling/importers/CsvImporter.js'

function findModelByName(domain: DataDomain, name: string): DomainModel {
  for (const model of domain.listModels()) {
    if (model.info.name === name || model.info.displayName === name) {
      return model
    }
  }
  throw new Error(`Model with name "${name}" not found`)
}

function findEntityByName(domain: DataDomain, name: string): DomainEntity {
  for (const entity of domain.listEntities()) {
    if (entity.info.name === name || entity.info.displayName === name) {
      return entity
    }
  }
  throw new Error(`Entity with name "${name}" not found`)
}

function findPropertyByName(entity: DomainEntity, name: string): DomainProperty {
  for (const prop of entity.listProperties()) {
    if (prop.info.name === name || prop.info.displayName === name) {
      return prop
    }
  }
  throw new Error(`Property with name "${name}" not found`)
}

test.group('CsvImporter: import', (group) => {
  let domain: DataDomain

  group.each.setup(() => {
    domain = new DataDomain()
  })

  test('should import a basic CSV structure', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResult: ParseResult = {
      format: [
        { index: 0, name: 'id', type: 'number', format: 'integer' },
        { index: 1, name: 'name', type: 'string' },
        { index: 2, name: 'value', type: 'number', format: 'decimal' },
      ],
      values: [[1, 'test-item', 99.9]],
      header: ['id', 'name', 'value'],
      file: 'products.csv',
    }

    await importer.import(parseResult, 'My Products')

    const model = findModelByName(domain, 'my_products')
    assert.exists(model)

    const entity = findEntityByName(domain, 'my_products')
    assert.exists(entity)
    assert.equal(entity!.info.displayName, 'My Products')

    const idProp = findPropertyByName(entity!, 'id')
    assert.exists(idProp)
    assert.equal(idProp!.type, 'number')
    assert.deepEqual(idProp!.bindings, [{ type: 'web', schema: { format: 'int64' } }])
    assert.deepEqual(idProp!.schema?.examples, ['1'])

    const nameProp = findPropertyByName(entity!, 'name')
    assert.exists(nameProp)
    assert.equal(nameProp!.type, 'string')
    assert.isEmpty(nameProp!.bindings)
    assert.deepEqual(nameProp!.schema?.examples, ['test-item'])
    assert.isUndefined(nameProp!.info.displayName, 'should not have displayName set')

    const valueProp = findPropertyByName(entity!, 'value')
    assert.exists(valueProp)
    assert.equal(valueProp!.type, 'number')
    assert.deepEqual(valueProp!.bindings, [{ type: 'web', schema: { format: 'double' } }])
    assert.deepEqual(valueProp!.schema?.examples, ['99.9'])
  })

  test('should handle name sanitization for model and properties', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResult: ParseResult = {
      format: [{ index: 0, name: 'First Name', type: 'string' }],
      values: [['John']],
      header: ['First Name'],
      file: 'users.csv',
    }

    await importer.import(parseResult, 'User List!')

    const model = findModelByName(domain, 'user_list')
    assert.exists(model)
    assert.equal(model!.info.displayName, 'User List!')

    const entity = findEntityByName(domain, 'user_list')
    assert.exists(entity)
    assert.equal(entity!.info.displayName, 'User List!')

    const prop = findPropertyByName(entity!, 'first_name')
    assert.exists(prop)
    assert.equal(prop!.info.displayName, 'First Name')
  })

  test('should import with no data rows (headers only)', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResult: ParseResult = {
      format: [
        { index: 0, name: 'id', type: 'number' },
        { index: 1, name: 'email', type: 'string' },
      ],
      values: [], // No data rows
      header: ['id', 'email'],
      file: 'empty.csv',
    }

    await importer.import(parseResult, 'users')

    const entity = findEntityByName(domain, 'users')
    assert.exists(entity)

    const idProp = findPropertyByName(entity!, 'id')
    assert.exists(idProp)
    assert.isUndefined(idProp!.schema?.examples, 'should have no examples')

    const emailProp = findPropertyByName(entity!, 'email')
    assert.exists(emailProp)
    assert.isUndefined(emailProp!.schema?.examples, 'should have no examples')
  })

  test('should handle null and undefined values in example row', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResult: ParseResult = {
      format: [
        { index: 0, name: 'col_a', type: 'string' },
        { index: 1, name: 'col_b', type: 'string' },
        { index: 2, name: 'col_c', type: 'string' },
      ],
      // @ts-expect-error TypeScript expects values to be defined
      values: [[null, 'has-value', undefined]],
      header: ['col_a', 'col_b', 'col_c'],
      file: 'test.csv',
    }

    await importer.import(parseResult, 'test_data')

    const entity = findEntityByName(domain, 'test_data')
    assert.exists(entity)

    const propA = findPropertyByName(entity!, 'col_a')
    assert.exists(propA)
    assert.isUndefined(propA!.schema?.examples, 'should not create example for null')

    const propB = findPropertyByName(entity!, 'col_b')
    assert.exists(propB)
    assert.deepEqual(propB!.schema?.examples, ['has-value'])

    const propC = findPropertyByName(entity!, 'col_c')
    assert.exists(propC)
    assert.isUndefined(propC!.schema?.examples, 'should not create example for undefined')
  })

  test('should handle integer and float number formats correctly', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResult: ParseResult = {
      format: [
        { index: 0, name: 'integer_val', type: 'number', format: 'integer' },
        { index: 1, name: 'float_val', type: 'number', format: 'decimal' },
      ],
      values: [[123, 45.67]],
      header: ['integer_val', 'float_val'],
      file: 'numeric.csv',
    }

    await importer.import(parseResult, 'Numeric Data')

    const entity = findEntityByName(domain, 'numeric_data')
    assert.exists(entity)

    const intProp = findPropertyByName(entity!, 'integer_val')
    assert.exists(intProp)
    assert.equal(intProp!.type, 'number')
    assert.deepEqual(intProp!.bindings, [{ type: 'web', schema: { format: 'int64' } }])

    const floatProp = findPropertyByName(entity!, 'float_val')
    assert.exists(floatProp)
    assert.equal(floatProp!.type, 'number')
    assert.deepEqual(floatProp!.bindings, [{ type: 'web', schema: { format: 'double' } }])
  })
})

test.group('CsvImporter: importMany', (group) => {
  let domain: DataDomain

  group.each.setup(() => {
    domain = new DataDomain()
  })

  test('should import multiple CSV structures into a single model', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResults: ParseResult[] = [
      {
        format: [
          { index: 0, name: 'id', type: 'number', format: 'integer' },
          { index: 1, name: 'name', type: 'string' },
        ],
        values: [
          [1, 'Product A'],
          [2, 'Product B'],
        ],
        header: ['id', 'name'],
        file: 'products.csv',
      },
      {
        format: [
          { index: 0, name: 'user_id', type: 'number', format: 'integer' },
          { index: 1, name: 'email', type: 'string' },
          { index: 2, name: 'active', type: 'boolean' },
        ],
        values: [
          [100, 'user@example.com', true],
          [101, 'admin@example.com', false],
        ],
        header: ['user_id', 'email', 'active'],
        file: 'users.csv',
      },
    ]

    const result = await importer.importMany(parseResults, 'E-commerce Data')

    // Check model
    const model = findModelByName(domain, 'e_commerce_data')
    assert.exists(model)
    assert.equal(model!.info.displayName, 'E-commerce Data')

    // Check entities
    assert.lengthOf(result.entities, 2)
    assert.equal(result.model, model)

    // Check products entity
    const productsEntity = findEntityByName(domain, 'products')
    assert.exists(productsEntity)
    assert.isUndefined(productsEntity!.info.displayName, 'the products entity should not have a displayName')

    const productIdProp = findPropertyByName(productsEntity!, 'id')
    assert.exists(productIdProp)
    assert.equal(productIdProp!.type, 'number')
    assert.deepEqual(productIdProp!.bindings, [{ type: 'web', schema: { format: 'int64' } }])
    assert.deepEqual(productIdProp!.schema?.examples, ['1'])

    const productNameProp = findPropertyByName(productsEntity!, 'name')
    assert.exists(productNameProp)
    assert.equal(productNameProp!.type, 'string')
    assert.deepEqual(productNameProp!.schema?.examples, ['Product A'])

    // Check users entity
    const usersEntity = findEntityByName(domain, 'users')
    assert.exists(usersEntity)
    assert.isUndefined(usersEntity!.info.displayName, 'the users entity should not have a displayName')

    const userIdProp = findPropertyByName(usersEntity!, 'user_id')
    assert.exists(userIdProp)
    assert.equal(userIdProp!.type, 'number')
    assert.deepEqual(userIdProp!.bindings, [{ type: 'web', schema: { format: 'int64' } }])
    assert.deepEqual(userIdProp!.schema?.examples, ['100'])

    const emailProp = findPropertyByName(usersEntity!, 'email')
    assert.exists(emailProp)
    assert.equal(emailProp!.type, 'string')
    assert.deepEqual(emailProp!.schema?.examples, ['user@example.com'])

    const activeProp = findPropertyByName(usersEntity!, 'active')
    assert.exists(activeProp)
    assert.equal(activeProp!.type, 'boolean')
    assert.deepEqual(activeProp!.schema?.examples, ['true'])
  })

  test('should handle name sanitization for model and entities', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResults: ParseResult[] = [
      {
        format: [{ index: 0, name: 'First Name', type: 'string' }],
        values: [['John']],
        header: ['First Name'],
        file: 'Employee List!',
      },
      {
        format: [{ index: 0, name: 'Department Name', type: 'string' }],
        values: [['Engineering']],
        header: ['Department Name'],
        file: 'Department Info@',
      },
    ]

    await importer.importMany(parseResults, 'HR Data!')

    const model = findModelByName(domain, 'hr_data')
    assert.exists(model)
    assert.equal(model!.info.displayName, 'HR Data!')

    const employeeEntity = findEntityByName(domain, 'employee_list')
    assert.exists(employeeEntity)
    assert.equal(employeeEntity!.info.displayName, 'Employee List!')

    const departmentEntity = findEntityByName(domain, 'department_info')
    assert.exists(departmentEntity)
    assert.equal(departmentEntity!.info.displayName, 'Department Info@')

    const firstNameProp = findPropertyByName(employeeEntity!, 'first_name')
    assert.exists(firstNameProp)
    assert.equal(firstNameProp!.info.displayName, 'First Name')

    const deptNameProp = findPropertyByName(departmentEntity!, 'department_name')
    assert.exists(deptNameProp)
    assert.equal(deptNameProp!.info.displayName, 'Department Name')
  })

  test('should handle empty data arrays for some entities', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResults: ParseResult[] = [
      {
        format: [
          { index: 0, name: 'id', type: 'number', format: 'integer' },
          { index: 1, name: 'title', type: 'string' },
        ],
        values: [[1, 'Sample Title']],
        header: ['id', 'title'],
        file: 'articles.csv',
      },
      {
        format: [
          { index: 0, name: 'tag_id', type: 'number' },
          { index: 1, name: 'tag_name', type: 'string' },
        ],
        values: [], // No data rows
        header: ['tag_id', 'tag_name'],
        file: 'tags.csv',
      },
    ]

    const result = await importer.importMany(parseResults, 'Blog Data')

    assert.lengthOf(result.entities, 2)

    const articlesEntity = findEntityByName(domain, 'articles')
    assert.exists(articlesEntity)

    const idProp = findPropertyByName(articlesEntity!, 'id')
    assert.exists(idProp)
    assert.deepEqual(idProp!.schema?.examples, ['1'])

    const tagsEntity = findEntityByName(domain, 'tags')
    assert.exists(tagsEntity)

    const tagIdProp = findPropertyByName(tagsEntity!, 'tag_id')
    assert.exists(tagIdProp)
    assert.isUndefined(tagIdProp!.schema?.examples, 'should have no examples for empty data')

    const tagNameProp = findPropertyByName(tagsEntity!, 'tag_name')
    assert.exists(tagNameProp)
    assert.isUndefined(tagNameProp!.schema?.examples, 'should have no examples for empty data')
  })

  test('should handle null and undefined values in example rows', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResults: ParseResult[] = [
      {
        format: [
          { index: 0, name: 'col_a', type: 'string' },
          { index: 1, name: 'col_b', type: 'string' },
        ],
        // @ts-expect-error TypeScript expects values to be defined
        values: [[null, 'valid-value']],
        header: ['col_a', 'col_b'],
        file: 'test_data.csv',
      },
      {
        format: [
          { index: 0, name: 'col_x', type: 'string' },
          { index: 1, name: 'col_y', type: 'string' },
        ],
        // @ts-expect-error TypeScript expects values to be defined
        values: [['another-value', undefined]],
        header: ['col_x', 'col_y'],
        file: 'more_test_data.csv',
      },
    ]

    const result = await importer.importMany(parseResults, 'Test Data')

    assert.lengthOf(result.entities, 2)

    const testDataEntity = findEntityByName(domain, 'test_data')
    assert.exists(testDataEntity)

    const propA = findPropertyByName(testDataEntity!, 'col_a')
    assert.exists(propA)
    assert.isUndefined(propA!.schema?.examples, 'should not create example for null')

    const propB = findPropertyByName(testDataEntity!, 'col_b')
    assert.exists(propB)
    assert.deepEqual(propB!.schema?.examples, ['valid-value'])

    const moreTestDataEntity = findEntityByName(domain, 'more_test_data')
    assert.exists(moreTestDataEntity)

    const propX = findPropertyByName(moreTestDataEntity!, 'col_x')
    assert.exists(propX)
    assert.deepEqual(propX!.schema?.examples, ['another-value'])

    const propY = findPropertyByName(moreTestDataEntity!, 'col_y')
    assert.exists(propY)
    assert.isUndefined(propY!.schema?.examples, 'should not create example for undefined')
  })

  test('should handle different number formats across entities', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResults: ParseResult[] = [
      {
        format: [
          { index: 0, name: 'int_val', type: 'number', format: 'integer' },
          { index: 1, name: 'decimal_val', type: 'number', format: 'decimal' },
        ],
        values: [[42, 3.14]],
        header: ['int_val', 'decimal_val'],
        file: 'numbers.csv',
      },
      {
        format: [
          { index: 0, name: 'price', type: 'number', format: 'decimal' },
          { index: 1, name: 'quantity', type: 'number', format: 'integer' },
        ],
        values: [[19.99, 5]],
        header: ['price', 'quantity'],
        file: 'inventory.csv',
      },
    ]

    const result = await importer.importMany(parseResults, 'Numeric Data')

    assert.lengthOf(result.entities, 2)

    const numbersEntity = findEntityByName(domain, 'numbers')
    assert.exists(numbersEntity)

    const intProp = findPropertyByName(numbersEntity!, 'int_val')
    assert.exists(intProp)
    assert.equal(intProp!.type, 'number')
    assert.deepEqual(intProp!.bindings, [{ type: 'web', schema: { format: 'int64' } }])

    const decimalProp = findPropertyByName(numbersEntity!, 'decimal_val')
    assert.exists(decimalProp)
    assert.equal(decimalProp!.type, 'number')
    assert.deepEqual(decimalProp!.bindings, [{ type: 'web', schema: { format: 'double' } }])

    const inventoryEntity = findEntityByName(domain, 'inventory')
    assert.exists(inventoryEntity)

    const priceProp = findPropertyByName(inventoryEntity!, 'price')
    assert.exists(priceProp)
    assert.equal(priceProp!.type, 'number')
    assert.deepEqual(priceProp!.bindings, [{ type: 'web', schema: { format: 'double' } }])

    const quantityProp = findPropertyByName(inventoryEntity!, 'quantity')
    assert.exists(quantityProp)
    assert.equal(quantityProp!.type, 'number')
    assert.deepEqual(quantityProp!.bindings, [{ type: 'web', schema: { format: 'int64' } }])
  })

  test('should handle single CSV data in array', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResults: ParseResult[] = [
      {
        format: [
          { index: 0, name: 'id', type: 'number', format: 'integer' },
          { index: 1, name: 'name', type: 'string' },
        ],
        values: [[1, 'Single Item']],
        header: ['id', 'name'],
        file: 'single.csv',
      },
    ]

    const result = await importer.importMany(parseResults, 'Single Data')

    const model = findModelByName(domain, 'single_data')
    assert.exists(model)
    assert.equal(model!.info.displayName, 'Single Data')

    assert.lengthOf(result.entities, 1)
    assert.equal(result.model, model)

    const singleEntity = findEntityByName(domain, 'single')
    assert.exists(singleEntity)
    assert.isUndefined(singleEntity!.info.displayName, 'the single entity should not have a displayName')

    const idProp = findPropertyByName(singleEntity!, 'id')
    assert.exists(idProp)
    assert.equal(idProp!.type, 'number')
    assert.deepEqual(idProp!.schema?.examples, ['1'])

    const nameProp = findPropertyByName(singleEntity!, 'name')
    assert.exists(nameProp)
    assert.equal(nameProp!.type, 'string')
    assert.deepEqual(nameProp!.schema?.examples, ['Single Item'])
  })

  test('should handle entities with fallback names when file names are missing', async ({ assert }) => {
    const importer = new CsvImporter(domain)
    const parseResults: ParseResult[] = [
      {
        format: [{ index: 0, name: 'col1', type: 'string' }],
        values: [['value1']],
        header: ['col1'],
        file: '',
      },
      {
        format: [{ index: 0, name: 'col2', type: 'string' }],
        values: [['value2']],
        header: ['col2'],
        file: '',
      },
    ]

    const result = await importer.importMany(parseResults, 'Fallback Data')

    assert.lengthOf(result.entities, 2)

    // Should use fallback names based on entity index
    const entity0 = findEntityByName(domain, 'entity_0')
    assert.exists(entity0)

    const entity1 = findEntityByName(domain, 'entity_1')
    assert.exists(entity1)
  })
})
