import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { signal, WritableSignal } from '@angular/core';
import { Router } from '@angular/router';
import { NoteService } from '../../features/note/note.service';
import { Note } from '../../features/note/note.model';
import { LayoutService } from '../../core-ui/layout/layout.service';
import { MenuTreeService } from '../../features/menu-tree/menu-tree.service';

import { SearchPageComponent } from './search-page.component';
import { TaskService } from '../../features/tasks/task.service';
import { ProjectService } from '../../features/project/project.service';
import { TagService } from '../../features/tag/tag.service';
import { NavigateToTaskService } from '../../core-ui/navigate-to-task/navigate-to-task.service';
import { BehaviorSubject } from 'rxjs';
import { Task } from '../../features/tasks/task.model';
import { Project } from '../../features/project/project.model';
import { Tag } from '../../features/tag/tag.model';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { SearchItem } from './search-page.model';

// Minimal stub task matching the Task interface shape
const createTask = (overrides: Partial<Task> = {}): Task =>
  ({
    id: 'task-1',
    title: 'Test Task',
    notes: '',
    parentId: null,
    projectId: null,
    tagIds: ['tag-1'],
    issueType: null,
    timeSpentOnDay: {},
    created: 1000,
    subTaskIds: [],
    isDone: false,
    ...overrides,
  }) as Task;

const createProject = (overrides: Partial<Project> = {}): Project =>
  ({
    id: 'proj-1',
    title: 'Test Project',
    icon: 'list',
    color: '#000',
    ...overrides,
  }) as Project;

const createTag = (overrides: Partial<Tag> = {}): Tag =>
  ({
    id: 'tag-1',
    title: 'Test Tag',
    icon: 'label',
    color: '#fff',
    ...overrides,
  }) as Tag;

describe('SearchPageComponent', () => {
  let fixture: ComponentFixture<SearchPageComponent>;
  let component: SearchPageComponent;

  let allTasks$: BehaviorSubject<Task[]>;
  let archivedTasks: Task[];
  let projectList$: BehaviorSubject<Project[]>;
  let tags$: BehaviorSubject<Tag[]>;
  let notes$: BehaviorSubject<Note[]>;
  let projectFolderMapSignal: WritableSignal<Map<string, string>>;
  let tagFolderMapSignal: WritableSignal<Map<string, string>>;

  let taskServiceSpy: jasmine.SpyObj<TaskService>;
  let navigateToTaskServiceSpy: jasmine.SpyObj<NavigateToTaskService>;
  let routerSpy: jasmine.SpyObj<Router>;
  let layoutServiceSpy: jasmine.SpyObj<LayoutService>;

  beforeEach(async () => {
    allTasks$ = new BehaviorSubject<Task[]>([]);
    archivedTasks = [];
    projectList$ = new BehaviorSubject<Project[]>([createProject()]);
    tags$ = new BehaviorSubject<Tag[]>([createTag()]);
    notes$ = new BehaviorSubject<Note[]>([]);
    projectFolderMapSignal = signal<Map<string, string>>(new Map());
    tagFolderMapSignal = signal<Map<string, string>>(new Map());

    taskServiceSpy = jasmine.createSpyObj('TaskService', ['getArchivedTasks'], {
      allTasks$,
    });
    taskServiceSpy.getArchivedTasks.and.callFake(() => Promise.resolve(archivedTasks));

    navigateToTaskServiceSpy = jasmine.createSpyObj('NavigateToTaskService', [
      'navigate',
    ]);
    navigateToTaskServiceSpy.navigate.and.returnValue(Promise.resolve());

    routerSpy = jasmine.createSpyObj('Router', ['navigate']);
    routerSpy.navigate.and.returnValue(Promise.resolve(true));

    layoutServiceSpy = jasmine.createSpyObj('LayoutService', ['toggleNotes'], {
      isShowNotes: signal(false),
    });

    await TestBed.configureTestingModule({
      imports: [SearchPageComponent, NoopAnimationsModule, TranslateModule.forRoot()],
      providers: [
        { provide: TaskService, useValue: taskServiceSpy },
        {
          provide: ProjectService,
          useValue: { list$: projectList$ },
        },
        { provide: TagService, useValue: { tags$: tags$ } },
        {
          provide: NavigateToTaskService,
          useValue: navigateToTaskServiceSpy,
        },
        { provide: NoteService, useValue: { notes$ } },
        { provide: Router, useValue: routerSpy },
        { provide: LayoutService, useValue: layoutServiceSpy },
        {
          provide: MenuTreeService,
          useValue: {
            projectFolderMap: projectFolderMapSignal,
            tagFolderMap: tagFolderMapSignal,
          },
        },
      ],
    })
      .overrideComponent(SearchPageComponent, {
        set: {
          template: '<input #inputEl>',
        },
      })
      .compileComponents();

    fixture = TestBed.createComponent(SearchPageComponent);
    component = fixture.componentInstance;
  });

  let latestResults: SearchItem[];

  const initAndFlush = (): void => {
    component.ngOnInit();
    // Subscribe early so we capture all emissions
    latestResults = [];
    component.filteredResults$.subscribe((r) => (latestResults = r));
    fixture.detectChanges();
    // Flush the archive promise (microtask)
    tick();
  };

  const typeAndFlush = (term: string): void => {
    component.searchForm.setValue(term);
    tick(150);
  };

  // --- Behavioral tests ---

  it('should return empty array for empty search', fakeAsync(() => {
    initAndFlush();
    typeAndFlush('');
    expect(latestResults).toEqual([]);
  }));

  it('should return empty array for whitespace-only search', fakeAsync(() => {
    initAndFlush();
    typeAndFlush('   ');
    expect(latestResults).toEqual([]);
  }));

  it('should find tasks by title (case-insensitive)', fakeAsync(() => {
    allTasks$.next([createTask({ id: 't1', title: 'Buy Groceries' })]);
    initAndFlush();
    typeAndFlush('buy');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].id).toBe('t1');
  }));

  it('should find tasks by notes (case-insensitive)', fakeAsync(() => {
    allTasks$.next([
      createTask({ id: 't1', title: 'My Task', notes: 'Remember to call Bob' }),
    ]);
    initAndFlush();
    typeAndFlush('BOB');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].id).toBe('t1');
  }));

  it('should limit results to 50', fakeAsync(() => {
    const tasks = Array.from({ length: 60 }, (_, i) =>
      createTask({ id: `t${i}`, title: `Match Task ${i}` }),
    );
    allTasks$.next(tasks);
    initAndFlush();
    typeAndFlush('Match');
    expect(latestResults.length).toBe(50);
  }));

  it('should debounce search by 150ms', fakeAsync(() => {
    allTasks$.next([createTask({ id: 't1', title: 'Hello World' })]);
    initAndFlush();

    component.searchForm.setValue('Hello');
    tick(100); // before debounce fires
    expect(latestResults).toEqual([]); // debounce hasn't fired yet

    tick(50); // now 150ms total
    expect(latestResults.length).toBe(1);
  }));

  it('should resolve subtask tagId from parent task', fakeAsync(() => {
    const parent = createTask({
      id: 'parent-1',
      title: 'Parent',
      tagIds: ['tag-parent'],
    });
    const child = createTask({
      id: 'child-1',
      title: 'Child Task',
      parentId: 'parent-1',
      tagIds: [],
    });
    tags$.next([createTag({ id: 'tag-parent', title: 'Parent Tag', icon: 'star' })]);
    allTasks$.next([parent, child]);
    initAndFlush();
    typeAndFlush('Child');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].tagId).toBe('tag-parent');
    expect(latestResults[0].parentTitle).toBe('Parent');
  }));

  it("should prefer subtask's own tagId over parent's when both have tags (#7756)", fakeAsync(() => {
    const parent = createTask({
      id: 'parent-2',
      title: 'Parent2',
      tagIds: ['tag-parent'],
    });
    const child = createTask({
      id: 'child-2',
      title: 'Tagged Child',
      parentId: 'parent-2',
      tagIds: ['tag-own'],
    });
    tags$.next([
      createTag({ id: 'tag-parent', title: 'Parent Tag', icon: 'star' }),
      createTag({ id: 'tag-own', title: 'Own Tag', icon: 'label' }),
    ]);
    allTasks$.next([parent, child]);
    initAndFlush();
    typeAndFlush('Tagged');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].tagId).toBe('tag-own');
  }));

  it('should use project context for tasks with projectId', fakeAsync(() => {
    projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);
    allTasks$.next([
      createTask({ id: 't1', title: 'Project Task', projectId: 'proj-1' }),
    ]);
    initAndFlush();
    typeAndFlush('Project');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].ctx.title).toBe('My Project');
  }));

  it('should use tag context for tasks without projectId', fakeAsync(() => {
    tags$.next([createTag({ id: 'tag-1', title: 'My Tag', icon: 'label' })]);
    allTasks$.next([
      createTask({
        id: 't1',
        title: 'Tag Task',
        projectId: '',
        tagIds: ['tag-1'],
      }),
    ]);
    initAndFlush();
    typeAndFlush('Tag');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].ctx.title).toBe('My Tag');
  }));

  it('should reflect isDone state on the result item (#7807)', fakeAsync(() => {
    const task = createTask({ id: 't1', title: 'Finish report', isDone: false });
    allTasks$.next([task]);
    initAndFlush();
    component.includeCompletedForm.setValue(true);
    tick(150);
    typeAndFlush('Finish');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].isDone).toBe(false);

    // Mark the task done — the search result must update without re-mounting
    allTasks$.next([{ ...task, isDone: true }]);
    tick(150);
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].isDone).toBe(true);
  }));

  it('should hide completed active tasks by default (#5943)', fakeAsync(() => {
    allTasks$.next([
      createTask({ id: 'open-task', title: 'Quarterly report', isDone: false }),
      createTask({ id: 'done-task', title: 'Quarterly report done', isDone: true }),
    ]);
    initAndFlush();
    typeAndFlush('Quarterly report');
    expect(latestResults.map((item) => item.id)).toEqual(['open-task']);
  }));

  it('should include completed active tasks when enabled (#5943)', fakeAsync(() => {
    allTasks$.next([
      createTask({ id: 'open-task', title: 'Quarterly report', isDone: false }),
      createTask({ id: 'done-task', title: 'Quarterly report done', isDone: true }),
    ]);
    initAndFlush();
    component.includeCompletedForm.setValue(true);
    tick(150);
    typeAndFlush('Quarterly report');
    expect(latestResults.map((item) => item.id)).toEqual(['open-task', 'done-task']);
  }));

  it('should mark archive tasks with isArchiveTask=true', fakeAsync(() => {
    // Must set archive tasks before creating a new component instance,
    // because _searchableItems$ calls getArchivedTasks() at construction time.
    const archiveTask = createTask({ id: 'archived-1', title: 'Old Task' });
    taskServiceSpy.getArchivedTasks.and.returnValue(Promise.resolve([archiveTask]));
    // Recreate component so the new archive promise is used
    fixture = TestBed.createComponent(SearchPageComponent);
    component = fixture.componentInstance;
    initAndFlush();
    component.includeCompletedForm.setValue(true);
    tick(150);
    typeAndFlush('Old');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].isArchiveTask).toBe(true);
  }));

  it('should hide archived tasks by default (#5943)', fakeAsync(() => {
    const archiveTask = createTask({ id: 'archived-1', title: 'Old Task' });
    taskServiceSpy.getArchivedTasks.and.returnValue(Promise.resolve([archiveTask]));
    fixture = TestBed.createComponent(SearchPageComponent);
    component = fixture.componentInstance;
    initAndFlush();
    typeAndFlush('Old');
    expect(latestResults).toEqual([]);
  }));

  it('should include archived tasks when completed tasks are enabled (#5943)', fakeAsync(() => {
    const archiveTask = createTask({ id: 'archived-1', title: 'Old Task' });
    taskServiceSpy.getArchivedTasks.and.returnValue(Promise.resolve([archiveTask]));
    fixture = TestBed.createComponent(SearchPageComponent);
    component = fixture.componentInstance;
    initAndFlush();
    component.includeCompletedForm.setValue(true);
    tick(150);
    typeAndFlush('Old');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].isArchiveTask).toBe(true);
  }));

  it('should call NavigateToTaskService.navigate on navigateToItem', () => {
    const item = {
      id: 'nav-1',
      isArchiveTask: true,
    } as SearchItem;
    component.navigateToItem(item);
    expect(navigateToTaskServiceSpy.navigate).toHaveBeenCalledWith('nav-1', true);
  });

  it('should reset form value on clearSearch', fakeAsync(() => {
    initAndFlush();
    component.searchForm.setValue('something');
    component.clearSearch();
    expect(component.searchForm.value).toBe('');
  }));

  it('should handle missing parent gracefully', fakeAsync(() => {
    // Child references a parent that is NOT in the tasks array
    const child = createTask({
      id: 'orphan-1',
      title: 'Orphan Child',
      parentId: 'nonexistent-parent',
      tagIds: ['tag-1'],
    });
    allTasks$.next([child]);
    initAndFlush();
    typeAndFlush('Orphan');
    expect(latestResults.length).toBe(1);
    // Should fall back to own tagIds instead of crashing
    expect(latestResults[0].tagId).toBe('tag-1');
    expect(latestResults[0].parentTitle).toBeNull();
  }));

  it('should handle missing context gracefully with fallback icon', fakeAsync(() => {
    // Task references a project that doesn't exist
    allTasks$.next([
      createTask({
        id: 't1',
        title: 'Lost Task',
        projectId: 'nonexistent-project',
      }),
    ]);
    projectList$.next([]);
    initAndFlush();
    typeAndFlush('Lost');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].ctx.icon).toBe('help_outline');
  }));

  it('should not crash when task.tagIds is undefined', fakeAsync(() => {
    allTasks$.next([
      createTask({ id: 't1', title: 'No Tags Task', tagIds: undefined as any }),
    ]);
    initAndFlush();
    typeAndFlush('No Tags');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].tagId).toBeUndefined();
  }));

  it('should not crash when parent.tagIds is undefined', fakeAsync(() => {
    // Robustness: parent.tagIds undefined must not crash the chip resolution.
    // With #7756 the child's own tags win when present, so the child gets its
    // own tag here. The no-crash guarantee comes from the optional chaining on
    // the fallback path (parent?.tagIds?.[0]).
    const parent = createTask({
      id: 'parent-1',
      title: 'Parent',
      tagIds: undefined as any,
    });
    const child = createTask({
      id: 'child-1',
      title: 'Child Bad Parent',
      parentId: 'parent-1',
      tagIds: ['tag-1'],
    });
    allTasks$.next([parent, child]);
    initAndFlush();
    typeAndFlush('Child Bad');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].tagId).toBe('tag-1');
  }));

  it('should not crash when both task.tagIds and parent.tagIds are undefined', fakeAsync(() => {
    const parent = createTask({
      id: 'parent-1',
      title: 'Parent',
      tagIds: undefined as any,
    });
    const child = createTask({
      id: 'child-1',
      title: 'Child No Tags',
      parentId: 'parent-1',
      tagIds: undefined as any,
    });
    allTasks$.next([parent, child]);
    initAndFlush();
    typeAndFlush('Child No');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].tagId).toBeUndefined();
  }));

  // --- Optimization-specific tests ---

  it('should include searchText on SearchItem (pre-computed lowercase)', fakeAsync(() => {
    allTasks$.next([createTask({ id: 't1', title: 'Hello World', notes: 'Some Notes' })]);
    initAndFlush();
    typeAndFlush('hello');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].searchText).toBeDefined();
    expect(latestResults[0].searchText).toContain('hello world');
    expect(latestResults[0].searchText).toContain('some notes');
  }));

  it('should find notes by content (case-insensitive)', fakeAsync(() => {
    notes$.next([
      {
        id: 'n1',
        content: 'Check out this important note\nSecond line',
        created: 1000,
      } as any,
    ]);
    initAndFlush();
    typeAndFlush('IMPORTANT');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].id).toBe('n1');
    expect(latestResults[0].title).toBe('Check out this important note');
    expect(latestResults[0].isNote).toBe(true);
  }));

  it('should navigate to note correctly in navigateToItem', fakeAsync(() => {
    const item = {
      id: 'n1',
      isNote: true,
      projectId: 'proj-1',
    } as SearchItem;
    component.navigateToItem(item);
    tick();
    expect(routerSpy.navigate).toHaveBeenCalledWith(['/project/proj-1/tasks'], {
      queryParams: { focusItem: 'n1' },
    });
    expect(layoutServiceSpy.toggleNotes).toHaveBeenCalled();
  }));

  it('should navigate to Today notes correctly in navigateToItem when projectId is null', fakeAsync(() => {
    const item = {
      id: 'n2',
      isNote: true,
      projectId: null,
    } as SearchItem;
    component.navigateToItem(item);
    tick();
    expect(routerSpy.navigate).toHaveBeenCalledWith(['/tag/TODAY/tasks'], {
      queryParams: { focusItem: 'n2' },
    });
    expect(layoutServiceSpy.toggleNotes).toHaveBeenCalled();
  }));

  it('should include full folder path in context tag title for task', fakeAsync(() => {
    projectFolderMapSignal.set(new Map([['proj-1', 'Folder 1 › Subfolder A']]));
    projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);
    allTasks$.next([createTask({ id: 't1', title: 'Folder Task', projectId: 'proj-1' })]);
    initAndFlush();
    typeAndFlush('Folder');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].ctx.title).toBe('Folder 1 > Subfolder A > My Project');
  }));

  it('should include full folder path in context tag title for note', fakeAsync(() => {
    projectFolderMapSignal.set(new Map([['proj-1', 'Folder 1 › Subfolder A']]));
    projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);
    notes$.next([
      {
        id: 'n1',
        content: 'Folder Note Content',
        projectId: 'proj-1',
        created: 1000,
      } as any,
    ]);
    initAndFlush();
    typeAndFlush('Folder Note');
    expect(latestResults.length).toBe(1);
    expect(latestResults[0].ctx.title).toBe('Folder 1 > Subfolder A > My Project');
  }));

  it('should update folder path reactively when projectFolderMap changes after init', fakeAsync(() => {
    projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);
    allTasks$.next([createTask({ id: 't1', title: 'Folder Task', projectId: 'proj-1' })]);
    initAndFlush();
    typeAndFlush('Folder');
    expect(latestResults[0].ctx.title).toBe('My Project');

    projectFolderMapSignal.set(new Map([['proj-1', 'Folder 1 › Subfolder A']]));
    fixture.detectChanges();
    tick(150); // combineLatest debounce
    expect(latestResults[0].ctx.title).toBe('Folder 1 > Subfolder A > My Project');
  }));

  it('should update folder path reactively for ARCHIVED tasks when projectFolderMap changes after init', fakeAsync(() => {
    const archiveTask = createTask({
      id: 'archived-1',
      title: 'Old Task',
      projectId: 'proj-1',
    });
    taskServiceSpy.getArchivedTasks.and.returnValue(Promise.resolve([archiveTask]));
    projectList$.next([createProject({ id: 'proj-1', title: 'My Project' })]);

    // Recreate component so the new archive promise is used
    fixture = TestBed.createComponent(SearchPageComponent);
    component = fixture.componentInstance;
    initAndFlush();
    component.includeCompletedForm.setValue(true);
    tick(150);
    typeAndFlush('Old');

    expect(latestResults.length).toBe(1);
    expect(latestResults[0].isArchiveTask).toBe(true);
    expect(latestResults[0].ctx.title).toBe('My Project');

    projectFolderMapSignal.set(new Map([['proj-1', 'Folder 1 › Subfolder A']]));
    fixture.detectChanges();
    tick(150); // combineLatest debounce
    expect(latestResults[0].ctx.title).toBe('Folder 1 > Subfolder A > My Project');
  }));
});
