import { type ICredentialType } from 'n8n-workflow';

import { clickCreateNewCredential, openCredentialSelect } from '../composables/ndv';
import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { AIAssistant } from '../pages/features/ai-assistant';
import { NodeCreator } from '../pages/features/node-creator';
import { getVisibleSelect } from '../utils';

const wf = new WorkflowPage();
const ndv = new NDV();
const aiAssistant = new AIAssistant();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
const nodeCreatorFeature = new NodeCreator();

describe('AI Assistant::disabled', () => {
	beforeEach(() => {
		aiAssistant.actions.disableAssistant();
		wf.actions.visit();
	});

	it('does not show assistant button if feature is disabled', () => {
		aiAssistant.getters.askAssistantFloatingButton().should('not.exist');
	});
});

describe('AI Assistant::enabled', () => {
	beforeEach(() => {
		aiAssistant.actions.enableAssistant();
		wf.actions.visit();
	});

	after(() => {
		aiAssistant.actions.disableAssistant();
	});

	it('renders placeholder UI', () => {
		aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
		aiAssistant.getters.askAssistantFloatingButton().click();
		aiAssistant.getters.askAssistantChat().should('be.visible');
		aiAssistant.getters.placeholderMessage().should('be.visible');
		aiAssistant.getters.chatInput().should('be.visible');
		aiAssistant.getters.sendMessageButton().should('be.disabled');
		aiAssistant.getters.closeChatButton().should('be.visible');
		aiAssistant.getters.closeChatButton().click();
		aiAssistant.getters.askAssistantChat().should('not.be.visible');
	});

	it('should resize assistant chat up', () => {
		aiAssistant.getters.askAssistantFloatingButton().click();
		aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
		aiAssistant.getters.askAssistantChat().then((element) => {
			const { width, left } = element[0].getBoundingClientRect();
			cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left - 10, 0], {
				abs: true,
				clickToFinish: true,
			});
			aiAssistant.getters.askAssistantChat().then((newElement) => {
				const newWidth = newElement[0].getBoundingClientRect().width;
				expect(newWidth).to.be.greaterThan(width);
			});
		});
	});

	it('should resize assistant chat down', () => {
		aiAssistant.getters.askAssistantFloatingButton().click();
		aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
		aiAssistant.getters.askAssistantChat().then((element) => {
			const { width, left } = element[0].getBoundingClientRect();
			cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left + 10, 0], {
				abs: true,
				clickToFinish: true,
			});
			aiAssistant.getters.askAssistantChat().then((newElement) => {
				const newWidth = newElement[0].getBoundingClientRect().width;
				expect(newWidth).to.be.lessThan(width);
			});
		});
	});

	it('should start chat session from node error view', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');
		cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
		wf.actions.openNode('Stop and Error');
		ndv.getters.nodeExecuteButton().click();
		aiAssistant.getters.nodeErrorViewAssistantButton().click();
		cy.wait('@chatRequest');
		aiAssistant.getters.chatMessagesAll().should('have.length', 1);
		aiAssistant.getters
			.chatMessagesAll()
			.eq(0)
			.should('contain.text', 'Hey, this is an assistant message');
		aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled');
	});

	it('should render chat input correctly', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');
		cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
		wf.actions.openNode('Stop and Error');
		ndv.getters.nodeExecuteButton().click();
		aiAssistant.getters.nodeErrorViewAssistantButton().click();
		cy.wait('@chatRequest');
		// Send button should be disabled when input is empty
		aiAssistant.getters.sendMessageButton().should('be.disabled');
		aiAssistant.getters.chatInput().type('Yo ');
		aiAssistant.getters.sendMessageButton().should('not.be.disabled');
		aiAssistant.getters.chatInput().then((element) => {
			const { height } = element[0].getBoundingClientRect();
			// Shift + Enter should add a new line
			aiAssistant.getters.chatInput().type('Hello{shift+enter}there');
			aiAssistant.getters.chatInput().then((newElement) => {
				const newHeight = newElement[0].getBoundingClientRect().height;
				// Chat input should grow as user adds new lines
				expect(newHeight).to.be.greaterThan(height);
				aiAssistant.getters.sendMessageButton().click();
				cy.wait('@chatRequest');
				// New lines should be rendered as <br> in the chat
				aiAssistant.getters.chatMessagesUser().should('have.length', 1);
				aiAssistant.getters.chatMessagesUser().eq(0).find('br').should('have.length', 1);
				// Chat input should be cleared now
				aiAssistant.getters.chatInput().should('have.value', '');
			});
		});
	});

	it('should render and handle quick replies', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/quick_reply_message_response.json',
		}).as('chatRequest');
		cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
		wf.actions.openNode('Stop and Error');
		ndv.getters.nodeExecuteButton().click();
		aiAssistant.getters.nodeErrorViewAssistantButton().click();
		cy.wait('@chatRequest');
		aiAssistant.getters.quickReplyButtons().should('have.length', 2);
		aiAssistant.getters.quickReplyButtons().eq(0).click();
		cy.wait('@chatRequest');
		aiAssistant.getters.chatMessagesUser().should('have.length', 1);
		aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
	});

	it('should warn before starting a new session', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');
		cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
		wf.actions.openNode('Edit Fields');
		ndv.getters.nodeExecuteButton().click();
		aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
		cy.wait('@chatRequest');
		aiAssistant.getters.closeChatButton().click();
		ndv.getters.backToCanvas().click();
		wf.actions.openNode('Stop and Error');
		ndv.getters.nodeExecuteButton().click();
		aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
		// Since we already have an active session, a warning should be shown
		aiAssistant.getters.newAssistantSessionModal().should('be.visible');
		aiAssistant.getters
			.newAssistantSessionModal()
			.find('button')
			.contains('Start new session')
			.click();
		cy.wait('@chatRequest');
		// New session should start with initial assistant message
		aiAssistant.getters.chatMessagesAll().should('have.length', 1);
	});

	it('should apply code diff to code node', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/code_diff_suggestion_response.json',
		}).as('chatRequest');
		cy.intercept('POST', '/rest/ai/chat/apply-suggestion', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/apply_code_diff_response.json',
		}).as('applySuggestion');
		cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
		wf.actions.openNode('Code');
		ndv.getters.nodeExecuteButton().click();
		aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
		cy.wait('@chatRequest');
		// Should have two assistant messages
		aiAssistant.getters.chatMessagesAll().should('have.length', 2);
		aiAssistant.getters.codeDiffs().should('have.length', 1);
		aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
		aiAssistant.getters.applyCodeDiffButtons().first().click();
		cy.wait('@applySuggestion');
		aiAssistant.getters.applyCodeDiffButtons().should('have.length', 0);
		aiAssistant.getters.undoReplaceCodeButtons().should('have.length', 1);
		aiAssistant.getters.codeReplacedMessage().should('be.visible');
		ndv.getters
			.parameterInput('jsCode')
			.get('.cm-content')
			.should('contain.text', 'item.json.myNewField = 1');
		// Clicking undo should revert the code back but not call the assistant
		aiAssistant.getters.undoReplaceCodeButtons().first().click();
		aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
		aiAssistant.getters.codeReplacedMessage().should('not.exist');
		cy.get('@applySuggestion.all').then((interceptions) => {
			expect(interceptions).to.have.length(1);
		});
		ndv.getters
			.parameterInput('jsCode')
			.get('.cm-content')
			.should('contain.text', 'item.json.myNewField = 1aaa');
		// Replacing the code again should also not call the assistant
		cy.get('@applySuggestion.all').then((interceptions) => {
			expect(interceptions).to.have.length(1);
		});
		aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
		aiAssistant.getters.applyCodeDiffButtons().first().click();
		ndv.getters
			.parameterInput('jsCode')
			.get('.cm-content')
			.should('contain.text', 'item.json.myNewField = 1');
	});

	it('Should ignore node execution success and error messages after the node run successfully once', () => {
		const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');

		const getEditor = () => getParameter().find('.cm-content').should('exist');

		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/code_diff_suggestion_response.json',
		}).as('chatRequest');

		cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
		wf.actions.openNode('Code');
		ndv.getters.nodeExecuteButton().click();
		aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
		cy.wait('@chatRequest');

		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/node_execution_succeeded_response.json',
		}).as('chatRequest2');

		getEditor()
			.type('{selectall}')
			.paste(
				'for (const item of $input.all()) {\n  item.json.myNewField = 1;\n}\n\nreturn $input.all();',
			);

		ndv.getters.nodeExecuteButton().click();

		getEditor()
			.type('{selectall}')
			.paste(
				'for (const item of $input.all()) {\n  item.json.myNewField = 1aaaa!;\n}\n\nreturn $input.all();',
			);

		ndv.getters.nodeExecuteButton().click();

		aiAssistant.getters.chatMessagesAssistant().should('have.length', 3);

		aiAssistant.getters
			.chatMessagesAssistant()
			.eq(2)
			.should(
				'contain.text',
				'Code node ran successfully, did my solution help resolve your issue?\nQuick reply 👇Yes, thanksNo, I am still stuck',
			);
	});

	it('should end chat session when `end_session` event is received', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/end_session_response.json',
		}).as('chatRequest');
		cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
		wf.actions.openNode('Stop and Error');
		ndv.getters.nodeExecuteButton().click();
		aiAssistant.getters.nodeErrorViewAssistantButton().click();
		cy.wait('@chatRequest');
		aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
		aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
	});

	it('should reset session after it ended and sidebar is closed', () => {
		cy.intercept('POST', '/rest/ai/chat', (req) => {
			req.reply((res) => {
				if (['init-support-chat'].includes(req.body.payload.type)) {
					res.send({
						statusCode: 200,
						fixture: 'aiAssistant/responses/simple_message_response.json',
					});
				} else {
					res.send({ statusCode: 200, fixture: 'aiAssistant/responses/end_session_response.json' });
				}
			});
		}).as('chatRequest');
		aiAssistant.actions.openChat();
		aiAssistant.actions.sendMessage('Hello');
		cy.wait('@chatRequest');
		aiAssistant.actions.closeChat();
		aiAssistant.actions.openChat();
		// After closing and reopening the chat, all messages should be still there
		aiAssistant.getters.chatMessagesAll().should('have.length', 2);
		// End the session
		aiAssistant.actions.sendMessage('Thanks, bye');
		cy.wait('@chatRequest');
		aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
		aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
		aiAssistant.actions.closeChat();
		aiAssistant.actions.openChat();
		// Now, session should be reset
		aiAssistant.getters.placeholderMessage().should('be.visible');
	});

	it('Should not reset assistant session when workflow is saved', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');
		wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
		aiAssistant.actions.openChat();
		aiAssistant.actions.sendMessage('Hello');
		wf.actions.openNode(SCHEDULE_TRIGGER_NODE_NAME);
		ndv.getters.nodeExecuteButton().click();
		wf.getters.isWorkflowSaved();
		aiAssistant.getters.placeholderMessage().should('not.exist');
	});

	it('should send message via enter even with global NodeCreator panel opened', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');

		wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
		aiAssistant.actions.openChat();
		nodeCreatorFeature.actions.openNodeCreator();
		aiAssistant.getters.chatInput().type('Hello{Enter}');

		aiAssistant.getters.placeholderMessage().should('not.exist');
	});
});

describe('AI Assistant Credential Help', () => {
	beforeEach(() => {
		aiAssistant.actions.enableAssistant();
		wf.actions.visit();
	});

	after(() => {
		aiAssistant.actions.disableAssistant();
	});

	it('should start credential help from node credential', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');
		wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
		wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
		wf.actions.openNode('Gmail');
		openCredentialSelect();
		clickCreateNewCredential();
		aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.visible');
		aiAssistant.getters.credentialEditAssistantButton().find('button').click();
		cy.wait('@chatRequest');
		aiAssistant.getters.chatMessagesUser().should('have.length', 1);
		aiAssistant.getters
			.chatMessagesUser()
			.eq(0)
			.should('contain.text', 'How do I set up the credentials for Gmail OAuth2 API?');

		aiAssistant.getters
			.chatMessagesAssistant()
			.eq(0)
			.should('contain.text', 'Hey, this is an assistant message');
		aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.disabled');
	});

	it('should start credential help from credential list', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');

		cy.visit(credentialsPage.url);
		credentialsPage.getters.emptyListCreateCredentialButton().click();

		credentialsModal.getters.newCredentialModal().should('be.visible');
		credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
		credentialsModal.getters.newCredentialTypeOption('Notion API').click();

		credentialsModal.getters.newCredentialTypeButton().click();

		aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.visible');
		aiAssistant.getters.credentialEditAssistantButton().find('button').click();
		cy.wait('@chatRequest');
		aiAssistant.getters.chatMessagesUser().should('have.length', 1);
		aiAssistant.getters
			.chatMessagesUser()
			.eq(0)
			.should('contain.text', 'How do I set up the credentials for Notion API?');

		aiAssistant.getters
			.chatMessagesAssistant()
			.eq(0)
			.should('contain.text', 'Hey, this is an assistant message');
		aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.disabled');
	});

	it('should not show assistant button when click to connect', () => {
		cy.intercept('/types/credentials.json', { middleware: true }, (req) => {
			req.headers['cache-control'] = 'no-cache, no-store';

			req.on('response', (res) => {
				const credentials: ICredentialType[] = res.body || [];

				const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');

				credentials[index] = {
					...credentials[index],
					__overwrittenProperties: ['clientId', 'clientSecret'],
				};
			});
		});

		wf.actions.visit(true);
		wf.actions.addNodeToCanvas('Manual');
		wf.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
		wf.getters.nodeCredentialsSelect().should('exist');
		wf.getters.nodeCredentialsSelect().click();
		getVisibleSelect().find('li').last().click();
		credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
		ndv.getters.copyInput().should('not.exist');
		credentialsModal.getters.oauthConnectButton().should('have.length', 1);
		credentialsModal.getters.credentialInputs().should('have.length', 0);
		aiAssistant.getters.credentialEditAssistantButton().should('not.exist');

		credentialsModal.getters.credentialAuthTypeRadioButtons().eq(1).click();
		credentialsModal.getters.credentialInputs().should('have.length', 1);
		aiAssistant.getters.credentialEditAssistantButton().should('exist');
	});

	it('should not show assistant button when click to connect with some fields', () => {
		cy.intercept('/types/credentials.json', { middleware: true }, (req) => {
			req.headers['cache-control'] = 'no-cache, no-store';

			req.on('response', (res) => {
				const credentials: ICredentialType[] = res.body || [];

				const index = credentials.findIndex((c) => c.name === 'microsoftOutlookOAuth2Api');

				credentials[index] = {
					...credentials[index],
					__overwrittenProperties: ['authUrl', 'accessTokenUrl', 'clientId', 'clientSecret'],
				};
			});
		});

		wf.actions.visit(true);
		wf.actions.addNodeToCanvas('Manual');
		wf.actions.addNodeToCanvas('Microsoft Outlook', true, true, 'Get a calendar');
		wf.getters.nodeCredentialsSelect().should('exist');
		wf.getters.nodeCredentialsSelect().click();
		getVisibleSelect().find('li').last().click();
		ndv.getters.copyInput().should('not.exist');
		credentialsModal.getters.oauthConnectButton().should('have.length', 1);
		credentialsModal.getters.credentialInputs().should('have.length', 1);
		aiAssistant.getters.credentialEditAssistantButton().should('not.exist');
	});
});

describe('General help', () => {
	beforeEach(() => {
		aiAssistant.actions.enableAssistant();
		wf.actions.visit();
	});

	it('assistant returns code snippet', () => {
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/code_snippet_response.json',
		}).as('chatRequest');

		aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
		aiAssistant.getters.askAssistantFloatingButton().click();
		aiAssistant.getters.askAssistantChat().should('be.visible');
		aiAssistant.getters.placeholderMessage().should('be.visible');
		aiAssistant.getters.chatInput().should('be.visible');

		aiAssistant.getters.chatInput().type('Show me an expression');
		aiAssistant.getters.sendMessageButton().click();

		aiAssistant.getters.chatMessagesAll().should('have.length', 3);
		aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', 'Show me an expression');

		aiAssistant.getters
			.chatMessagesAssistant()
			.eq(0)
			.should('contain.text', 'To use expressions in n8n, follow these steps:');

		aiAssistant.getters
			.chatMessagesAssistant()
			.eq(0)
			.should(
				'include.html',
				`<pre><code class="language-json">[
  {
    "headers": {
      "host": "n8n.instance.address",
      ...
    },
    "params": {},
    "query": {},
    "body": {
      "name": "Jim",
      "age": 30,
      "city": "New York"
    }
  }
]
</code></pre>`,
			);
		aiAssistant.getters.codeSnippet().should('have.text', '{{$json.body.city}}');
	});

	it('should send current context to support chat', () => {
		cy.createFixtureWorkflow('aiAssistant/workflows/simple_http_request_workflow.json');
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');

		aiAssistant.getters.askAssistantFloatingButton().click();
		aiAssistant.actions.sendMessage('What is wrong with this workflow?');

		cy.wait('@chatRequest').then((interception) => {
			const { body } = interception.request;
			// Body should contain the current workflow context
			expect(body.payload).to.have.property('context');
			expect(body.payload.context).to.have.property('currentView');
			expect(body.payload.context.currentView.name).to.equal('NodeViewExisting');
			expect(body.payload.context).to.have.property('currentWorkflow');
		});
	});

	it('should not send workflow context if nothing changed', () => {
		cy.createFixtureWorkflow('aiAssistant/workflows/simple_http_request_workflow.json');
		cy.intercept('POST', '/rest/ai/chat', {
			statusCode: 200,
			fixture: 'aiAssistant/responses/simple_message_response.json',
		}).as('chatRequest');

		aiAssistant.getters.askAssistantFloatingButton().click();
		wf.getters.zoomToFitButton().click();

		aiAssistant.actions.sendMessage('What is wrong with this workflow?');
		cy.wait('@chatRequest');

		// Send another message without changing workflow or executing any node
		aiAssistant.actions.sendMessage('And now?');

		cy.wait('@chatRequest').then((interception) => {
			const { body } = interception.request;
			// Workflow context should be empty
			expect(body.payload).to.have.property('context');
			expect(body.payload.context).not.to.have.property('currentWorkflow');
		});

		// Update http request node url
		wf.actions.openNode('HTTP Request');
		ndv.actions.typeIntoParameterInput('url', 'https://example.com');
		ndv.actions.close();
		// Also execute the workflow
		wf.actions.executeWorkflow();

		// Send another message
		aiAssistant.actions.sendMessage('What about now?');
		cy.wait('@chatRequest').then((interception) => {
			const { body } = interception.request;
			// Both workflow and execution context should be sent
			expect(body.payload).to.have.property('context');
			expect(body.payload.context).to.have.property('currentWorkflow');
			expect(body.payload.context.currentWorkflow).not.to.be.empty;
			expect(body.payload.context).to.have.property('executionData');
			expect(body.payload.context.executionData).not.to.be.empty;
		});
	});
});