// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { expect } from 'chai';
import { Container } from '@theia/core/shared/inversify';
import { ChatServiceImpl } from './chat-service';
import { ChatSessionStore, ChatSessionIndex, ChatModelWithMetadata } from './chat-session-store';
import { ChatAgentService } from './chat-agent-service';
import { ChatRequestParser } from './chat-request-parser';
import { AIVariableService, ToolInvocationRegistry } from '@theia/ai-core';
import { ILogger } from '@theia/core';
import { ChatContentDeserializerRegistry, ChatContentDeserializerRegistryImpl, DefaultChatContentDeserializerContribution } from './chat-content-deserializer';
import { ChangeSetElementDeserializerRegistry, ChangeSetElementDeserializerRegistryImpl } from './change-set-element-deserializer';
import { ChatAgentLocation } from './chat-agents';
import { ChatModel } from './chat-model';
import { SerializedChatData } from './chat-model-serialization';

describe('ChatService Session Deletion', () => {
    let chatService: ChatServiceImpl;
    let sessionStore: MockChatSessionStore;
    let container: Container;

    class MockChatSessionStore implements ChatSessionStore {
        public deletedSessions: string[] = [];
        public storedSessions: Array<ChatModel | ChatModelWithMetadata> = [];

        async storeSessions(...sessions: Array<ChatModel | ChatModelWithMetadata>): Promise<void> {
            this.storedSessions = sessions;
        }

        async readSession(sessionId: string): Promise<SerializedChatData | undefined> {
            return undefined;
        }

        async deleteSession(sessionId: string): Promise<void> {
            this.deletedSessions.push(sessionId);
        }

        async clearAllSessions(): Promise<void> {
            this.deletedSessions = [];
            this.storedSessions = [];
        }

        async getSessionIndex(): Promise<ChatSessionIndex> {
            return {};
        }

        async hasPersistedSessions(): Promise<boolean> {
            return false;
        }

        async setSessionTitle(sessionId: string, title: string): Promise<void> {
        }
    }

    class MockChatAgentService {
        readonly onDidChangeAgents = { dispose: () => { } };
        readonly onDefaultAgentChanged = { dispose: () => { } };

        getAgent(): undefined {
            return undefined;
        }
        getAgents(): never[] {
            return [];
        }
        resolveAgent(): undefined {
            return undefined;
        }
    }

    class MockChatRequestParser {
        parseChatRequest(): { parts: never[]; text: string } {
            return { parts: [], text: '' };
        }
    }

    class MockAIVariableService {
        resolveVariables(): Promise<unknown[]> {
            return Promise.resolve([]);
        }
    }

    class MockLogger {
        error(): void { }
        warn(): void { }
        info(): void { }
        debug(): void { }
    }

    const mockToolInvocationRegistry: ToolInvocationRegistry = {
        registerTool: () => { },
        unregisterTool: () => { },
        getFunction: () => undefined,
        getFunctions: () => [],
        getAllFunctions: () => [],
        unregisterAllTools: () => { },
        onDidChange: () => ({ dispose: () => { } })
    };

    beforeEach(() => {
        container = new Container();
        sessionStore = new MockChatSessionStore();

        container.bind(ChatSessionStore).toConstantValue(sessionStore);
        container.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
        container.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
        container.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
        container.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
        container.bind(ToolInvocationRegistry).toConstantValue(mockToolInvocationRegistry);

        // Bind deserializer registries
        const contentRegistry = new ChatContentDeserializerRegistryImpl();
        new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
        container.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
        container.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());
        container.bind(ChatServiceImpl).toSelf().inSingletonScope();

        chatService = container.get(ChatServiceImpl);
    });

    describe('deleteSession', () => {
        it('should delete session from memory and persistent storage', async () => {
            // Create a session
            const session = chatService.createSession(ChatAgentLocation.Panel);
            expect(chatService.getSessions()).to.have.lengthOf(1);

            // Delete the session (now returns a Promise)
            await chatService.deleteSession(session.id);

            // Verify it's removed from memory
            expect(chatService.getSessions()).to.have.lengthOf(0);

            // Verify it's deleted from persistent storage
            expect(sessionStore.deletedSessions).to.include(session.id);
        });

        it('should emit SessionDeletedEvent when session is deleted', done => {
            // Create a session
            const session = chatService.createSession(ChatAgentLocation.Panel);

            // Listen for deletion event
            chatService.onSessionEvent(event => {
                if (event.type === 'deleted') {
                    expect(event.sessionId).to.equal(session.id);
                    done();
                }
            });

            // Delete the session
            chatService.deleteSession(session.id);
        });

        it('should handle deletion when session store is not available', async () => {
            // Create a new service without session store
            const containerWithoutStore = new Container();
            containerWithoutStore.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
            containerWithoutStore.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
            containerWithoutStore.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
            containerWithoutStore.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
            containerWithoutStore.bind(ToolInvocationRegistry).toConstantValue(mockToolInvocationRegistry);

            // Bind deserializer registries
            const contentRegistry = new ChatContentDeserializerRegistryImpl();
            new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
            containerWithoutStore.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
            containerWithoutStore.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());

            containerWithoutStore.bind(ChatServiceImpl).toSelf().inSingletonScope();

            const serviceWithoutStore = containerWithoutStore.get(ChatServiceImpl);

            // Create and delete a session - should not throw
            const session = serviceWithoutStore.createSession(ChatAgentLocation.Panel);
            await serviceWithoutStore.deleteSession(session.id);

            // Verify session is still removed from memory
            expect(serviceWithoutStore.getSessions()).to.have.lengthOf(0);
        });

        it('should attempt storage deletion even for non-existent in-memory sessions', async () => {
            const initialDeletedCount = sessionStore.deletedSessions.length;
            const nonExistentId = 'non-existent-id';

            // Try to delete non-existent session (could be a persisted-only session)
            await chatService.deleteSession(nonExistentId);

            // Verify storage deletion was attempted even though session not in memory
            expect(sessionStore.deletedSessions).to.include(nonExistentId);
            expect(sessionStore.deletedSessions).to.have.lengthOf(initialDeletedCount + 1);
        });

        it('should handle deleting active session', () => {
            // Create two sessions
            const session1 = chatService.createSession(ChatAgentLocation.Panel);
            const session2 = chatService.createSession(ChatAgentLocation.Panel);

            // Ensure session2 is active (it should be by default as the latest)
            expect(chatService.getActiveSession()?.id).to.equal(session2.id);

            // Delete session1 (not active)
            chatService.deleteSession(session1.id);

            // Verify session2 is still active
            const activeSession = chatService.getActiveSession();
            expect(activeSession).to.not.be.undefined;
            expect(activeSession?.id).to.equal(session2.id);
        });

        it('should handle storage deletion errors gracefully', async () => {
            // Create a session store that throws errors
            const errorStore = new MockChatSessionStore();
            errorStore.deleteSession = async () => {
                throw new Error('Storage error');
            };

            const errorContainer = new Container();
            errorContainer.bind(ChatSessionStore).toConstantValue(errorStore);
            errorContainer.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
            errorContainer.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
            errorContainer.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
            errorContainer.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
            errorContainer.bind(ToolInvocationRegistry).toConstantValue(mockToolInvocationRegistry);

            // Bind deserializer registries
            const contentRegistry = new ChatContentDeserializerRegistryImpl();
            new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
            errorContainer.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
            errorContainer.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());

            errorContainer.bind(ChatServiceImpl).toSelf().inSingletonScope();

            const errorService = errorContainer.get(ChatServiceImpl);

            // Create and delete a session - should not throw even with storage error
            const session = errorService.createSession(ChatAgentLocation.Panel);
            await errorService.deleteSession(session.id);

            // Verify session is still removed from memory despite storage error
            expect(errorService.getSessions()).to.have.lengthOf(0);
        });

        it('should delete persisted-only sessions (not loaded in memory)', async () => {
            // This simulates deleting a session from the "Show Chats..." dialog
            // when the session is persisted but not currently loaded into memory
            const persistedSessionId = 'persisted-session-123';

            // Verify session is not in memory
            expect(chatService.getSessions().find(s => s.id === persistedSessionId)).to.be.undefined;

            // Delete the persisted-only session
            await chatService.deleteSession(persistedSessionId);

            // Verify it was still deleted from storage (even though not in memory)
            expect(sessionStore.deletedSessions).to.include(persistedSessionId);
        });

        it('should cascade-delete descendants when deleting an intermediate session (A -> B -> C, in memory)', async () => {
            // Delegation chain A -> B -> C, all loaded in memory. C's rootSessionId points at A (the
            // root of the chain), but its immediate parent is B, so deleting B must still remove C.
            const a = chatService.createSession(ChatAgentLocation.Panel);
            const b = chatService.createSession(ChatAgentLocation.Panel);
            const c = chatService.createSession(ChatAgentLocation.Panel);
            const internal = b as unknown as { rootSessionId?: string; parentSessionId?: string };
            internal.rootSessionId = a.id;
            internal.parentSessionId = a.id;
            const cInternal = c as unknown as { rootSessionId?: string; parentSessionId?: string };
            cInternal.rootSessionId = a.id;
            cInternal.parentSessionId = b.id;

            await chatService.deleteSession(b.id);

            const remaining = chatService.getSessions().map(s => s.id);
            expect(remaining).to.include(a.id);
            expect(remaining).to.not.include(b.id);
            expect(remaining).to.not.include(c.id);
            expect(sessionStore.deletedSessions).to.include(b.id);
            expect(sessionStore.deletedSessions).to.include(c.id);
        });

        it('should cascade-delete persisted-only descendants of an intermediate session', async () => {
            // The whole chain lives only in the persisted index (e.g. after a reload). Deleting the
            // intermediate B must reach C via its parentSessionId even though C's rootSessionId is A.
            sessionStore.getSessionIndex = async (): Promise<ChatSessionIndex> => ({
                A: { sessionId: 'A', title: 'A', saveDate: 0, location: ChatAgentLocation.Panel },
                B: { sessionId: 'B', title: 'B', saveDate: 0, location: ChatAgentLocation.Panel, rootSessionId: 'A', parentSessionId: 'A' },
                C: { sessionId: 'C', title: 'C', saveDate: 0, location: ChatAgentLocation.Panel, rootSessionId: 'A', parentSessionId: 'B' }
            });

            await chatService.deleteSession('B');

            expect(sessionStore.deletedSessions).to.include('B');
            expect(sessionStore.deletedSessions).to.include('C');
            expect(sessionStore.deletedSessions).to.not.include('A');
        });

        it('should not loop forever on a cyclic parent reference', async () => {
            // Defensive: a corrupted index where two sessions reference each other must still terminate.
            sessionStore.getSessionIndex = async (): Promise<ChatSessionIndex> => ({
                X: { sessionId: 'X', title: 'X', saveDate: 0, location: ChatAgentLocation.Panel, parentSessionId: 'Y' },
                Y: { sessionId: 'Y', title: 'Y', saveDate: 0, location: ChatAgentLocation.Panel, parentSessionId: 'X' }
            });

            await chatService.deleteSession('X');

            expect(sessionStore.deletedSessions).to.include('X');
            expect(sessionStore.deletedSessions).to.include('Y');
        });

        it('should fire SessionDeletedEvent for persisted-only sessions', async () => {
            // When deleting a persisted-only session (not in memory),
            // we still fire the event so consumers (e.g. the Welcome view's chat cards)
            // can remove the entry.
            const persistedSessionId = 'persisted-session-456';
            let eventFired = false;

            // Listen for deletion event
            chatService.onSessionEvent(event => {
                if (event.type === 'deleted' && event.sessionId === persistedSessionId) {
                    eventFired = true;
                }
            });

            // Delete the persisted-only session
            await chatService.deleteSession(persistedSessionId);

            // Event should have been fired so the Welcome view can remove the entry
            expect(eventFired).to.be.true;
            // Storage deletion should also have happened
            expect(sessionStore.deletedSessions).to.include(persistedSessionId);
        });
    });
});
