import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
  ComponentFixture,
  fakeAsync,
  TestBed,
  tick,
} from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import {
  CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
  CustomFieldQueryLogicalOperator,
  CustomFieldQueryOperatorGroups,
} from 'src/app/data/custom-field-query'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import {
  CustomFieldQueryAtom,
  CustomFieldQueryElement,
  CustomFieldQueryExpression,
} from 'src/app/utils/custom-field-query-element'
import {
  CustomFieldQueriesModel,
  CustomFieldsQueryDropdownComponent,
} from './custom-fields-query-dropdown.component'

const customFields = [
  {
    id: 1,
    name: 'Test Field',
    data_type: CustomFieldDataType.String,
    extra_data: {},
  },
  {
    id: 2,
    name: 'Test Select Field',
    data_type: CustomFieldDataType.Select,
    extra_data: {
      select_options: [
        { label: 'Option 1', id: 'abc-123' },
        { label: 'Option 2', id: 'def-456' },
      ],
    },
  },
]

describe('CustomFieldsQueryDropdownComponent', () => {
  let component: CustomFieldsQueryDropdownComponent
  let fixture: ComponentFixture<CustomFieldsQueryDropdownComponent>
  let customFieldsService: CustomFieldsService

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [
        NgbDropdownModule,
        NgxBootstrapIconsModule.pick(allIcons),
        NgSelectModule,
        FormsModule,
        ReactiveFormsModule,
        CustomFieldsQueryDropdownComponent,
      ],
      providers: [
        provideHttpClient(withInterceptorsFromDi()),
        provideHttpClientTesting(),
      ],
    }).compileComponents()

    customFieldsService = TestBed.inject(CustomFieldsService)
    jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
      of({
        count: customFields.length,
        all: customFields.map((f) => f.id),
        results: customFields,
      })
    )
    fixture = TestBed.createComponent(CustomFieldsQueryDropdownComponent)
    component = fixture.componentInstance
    component.icon = 'ui-radios'
    fixture.detectChanges()
  })

  it('should initialize custom fields on creation', () => {
    expect(component.customFields).toEqual(customFields)
  })

  it('should add an expression when opened if queries are empty', () => {
    component.selectionModel.clear()
    component.onOpenChange(true)
    expect(component.selectionModel.queries.length).toBe(1)
  })

  it('should support reset the selection model', () => {
    component.selectionModel.addExpression()
    component.reset()
    expect(component.selectionModel.isEmpty()).toBeTruthy()
  })

  it('should get operators for a field', () => {
    const field: CustomField = {
      id: 1,
      name: 'Test Field',
      data_type: CustomFieldDataType.String,
      extra_data: {},
    }
    component.customFields = [field]
    const operators = component.getOperatorsForField(1)
    expect(operators.length).toEqual(
      [
        ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
          CustomFieldQueryOperatorGroups.Basic
        ],
        ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
          CustomFieldQueryOperatorGroups.Exact
        ],
        ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
          CustomFieldQueryOperatorGroups.String
        ],
      ].length
    )

    // Fallback to basic operators if field is not found
    const operators2 = component.getOperatorsForField(2)
    expect(operators2.length).toEqual(
      CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
        CustomFieldQueryOperatorGroups.Basic
      ].length
    )
  })

  it('should get select options for a field', () => {
    const field: CustomField = {
      id: 1,
      name: 'Test Field',
      data_type: CustomFieldDataType.Select,
      extra_data: {
        select_options: [
          { label: 'Option 1', id: 'abc-123' },
          { label: 'Option 2', id: 'def-456' },
        ],
      },
    }
    component.customFields = [field]
    const options = component.getSelectOptionsForField(1)
    expect(options).toEqual([
      { label: 'Option 1', id: 'abc-123' },
      { label: 'Option 2', id: 'def-456' },
    ])

    // Fallback to empty array if field is not found
    const options2 = component.getSelectOptionsForField(2)
    expect(options2).toEqual([])
  })

  it('should remove an element from the selection model', () => {
    const expression = new CustomFieldQueryExpression()
    const atom = new CustomFieldQueryAtom()
    ;(expression.value as CustomFieldQueryElement[]).push(atom)
    component.selectionModel.addExpression(expression)
    component.removeElement(atom)
    expect(component.selectionModel.isEmpty()).toBeTruthy()
    const expression2 = new CustomFieldQueryExpression([
      CustomFieldQueryLogicalOperator.And,
      [
        [1, 'icontains', 'test'],
        [2, 'icontains', 'test'],
      ],
    ])
    component.selectionModel.addExpression(expression2)
    component.removeElement(expression2)
    expect(component.selectionModel.isEmpty()).toBeTruthy()
  })

  it('should emit selectionModelChange when model changes', () => {
    const nextSpy = jest.spyOn(component.selectionModelChange, 'next')
    const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
    component.selectionModel.addAtom(atom)
    atom.changed.next(atom)
    expect(nextSpy).toHaveBeenCalled()
  })

  it('should complete selection model subscription when new selection model is set', () => {
    const completeSpy = jest.spyOn(component.selectionModel.changed, 'complete')
    const selectionModel = new CustomFieldQueriesModel()
    component.selectionModel = selectionModel
    expect(completeSpy).toHaveBeenCalled()
  })

  it('should support adding an atom', () => {
    const expression = new CustomFieldQueryExpression()
    component.addAtom(expression)
    expect(expression.value.length).toBe(1)
  })

  it('should support adding an expression', () => {
    const expression = new CustomFieldQueryExpression()
    component.addExpression(expression)
    expect(expression.value.length).toBe(1)
  })

  it('should support getting a custom field by ID', () => {
    expect(component.getCustomFieldByID(1)).toEqual(customFields[0])
  })

  it('should sanitize name from title', () => {
    component.title = 'Test Title'
    expect(component.name).toBe('test_title')
  })

  it('should add a default atom on open and focus the select field', fakeAsync(() => {
    expect(component.selectionModel.queries.length).toBe(0)
    component.onOpenChange(true)
    fixture.detectChanges()
    tick()
    expect(component.selectionModel.queries.length).toBe(1)
    expect(window.document.activeElement.tagName).toBe('INPUT')
  }))

  describe('CustomFieldQueriesModel', () => {
    let model: CustomFieldQueriesModel

    beforeEach(() => {
      model = new CustomFieldQueriesModel()
    })

    it('should initialize with empty queries', () => {
      expect(model.queries).toEqual([])
    })

    it('should clear queries and fire event', () => {
      const nextSpy = jest.spyOn(model.changed, 'next')
      model.addExpression()
      model.clear()
      expect(model.queries).toEqual([])
      expect(nextSpy).toHaveBeenCalledWith(model)
    })

    it('should clear queries without firing event', () => {
      const nextSpy = jest.spyOn(model.changed, 'next')
      model.addExpression()
      model.clear(false)
      expect(model.queries).toEqual([])
      expect(nextSpy).not.toHaveBeenCalled()
    })

    it('should validate an empty model as invalid', () => {
      expect(model.isValid()).toBeFalsy()
    })

    it('should validate a model with valid expression as valid', () => {
      const expression = new CustomFieldQueryExpression()
      const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
      const atom2 = new CustomFieldQueryAtom([2, 'icontains', 'test'])
      const expression2 = new CustomFieldQueryExpression()
      expression2.addAtom(atom)
      expression2.addAtom(atom2)
      expression.addExpression(expression2)
      model.addExpression(expression)
      expect(model.isValid()).toBeTruthy()
    })

    it('should validate a model with invalid expression as invalid', () => {
      const expression = new CustomFieldQueryExpression()
      model.addExpression(expression)
      expect(model.isValid()).toBeFalsy()
    })

    it('should validate an atom with in or contains operator', () => {
      const atom = new CustomFieldQueryAtom([1, 'in', '[1,2,3]'])
      expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
      atom.operator = 'contains'
      atom.value = [1, 2, 3]
      expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
      atom.value = null
      expect(model['validateAtom'].apply(null, [atom])).toBeFalsy()
    })

    it('should check if model is empty', () => {
      expect(model.isEmpty()).toBeTruthy()
      model.addExpression()
      expect(model.isEmpty()).toBeTruthy()
      const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
      model.addAtom(atom)
      expect(model.isEmpty()).toBeFalsy()
    })

    it('should add an atom to the model', () => {
      const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
      model.addAtom(atom)
      expect(model.queries.length).toBe(1)
      expect(
        (model.queries[0] as CustomFieldQueryExpression).value.length
      ).toBe(1)
    })

    it('should add an expression to the model, propagate changes', () => {
      const expression = new CustomFieldQueryExpression()
      model.addExpression(expression)
      expect(model.queries.length).toBe(1)
      const expression2 = new CustomFieldQueryExpression([
        CustomFieldQueryLogicalOperator.And,
        [
          [1, 'icontains', 'test'],
          [2, 'icontains', 'test'],
        ],
      ])
      model.addExpression(expression2)
      const nextSpy = jest.spyOn(model.changed, 'next')
      expression2.changed.next(expression2)
      expect(nextSpy).toHaveBeenCalled()
    })

    it('should remove an element from the model', () => {
      const expression = new CustomFieldQueryExpression([
        CustomFieldQueryLogicalOperator.And,
        [
          [1, 'icontains', 'test'],
          [2, 'icontains', 'test'],
        ],
      ])
      const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
      const expression2 = new CustomFieldQueryExpression([
        CustomFieldQueryLogicalOperator.And,
        [
          [3, 'icontains', 'test'],
          [4, 'icontains', 'test'],
        ],
      ])
      expression.addAtom(atom)
      expression2.addExpression(expression)
      model.addExpression(expression2)
      model.removeElement(atom)
      expect(model.queries.length).toBe(1)
      model.removeElement(expression2)
    })

    it('should fire changed event when an atom changes', () => {
      const nextSpy = jest.spyOn(model.changed, 'next')
      const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
      model.addAtom(atom)
      atom.changed.next(atom)
      expect(nextSpy).toHaveBeenCalledWith(model)
    })

    it('should complete changed subject when element is removed', () => {
      const expression = new CustomFieldQueryExpression()
      const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
      ;(expression.value as CustomFieldQueryElement[]).push(atom)
      model.addExpression(expression)
      const completeSpy = jest.spyOn(atom.changed, 'complete')
      model.removeElement(atom)
      expect(completeSpy).toHaveBeenCalled()
    })

    it('should subscribe to existing elements when queries are assigned', () => {
      const expression = new CustomFieldQueryExpression()
      const nextSpy = jest.spyOn(model.changed, 'next')
      model.queries = [expression]
      expression.changed.next(expression)
      expect(nextSpy).toHaveBeenCalledWith(model)
    })
  })
})
