import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils';
import { setActivePinia, createPinia } from 'pinia';
import { createRouter, createMemoryHistory } from 'vue-router';
import SettingsView from '@/views/SettingsView.vue';
import { useContextStore } from '@/stores/context';
import type {
  ProjectResponse, OrganizationResponse, EnvironmentResponse,
  UserPermissionResponse, ApiKeyResponse, CreateApiKeyResponse,
} from '@/types/api';

jest.mock('@/api/organizations');
jest.mock('@/api/projects');
jest.mock('@/api/environments');
jest.mock('@/api/apiKeys');
jest.mock('@/api/webhooks');
jest.mock('@/api/client', () => ({
  setAuthTokens: jest.fn(),
  clearAuthTokens: jest.fn(),
  getAccessToken: jest.fn(),
}));

import * as orgsApi from '@/api/organizations';
import * as projectsApi from '@/api/projects';
import * as environmentsApi from '@/api/environments';
import * as apiKeysApi from '@/api/apiKeys';
import * as webhooksApi from '@/api/webhooks';

const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
const mockProject: ProjectResponse = { id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z' };
const mockEnvs: EnvironmentResponse[] = [
  { id: 100, name: 'Development', apiKey: 'env-dev', projectId: 10, createdAt: '2026-01-01T00:00:00Z' },
  { id: 200, name: 'Production', apiKey: 'env-prod', projectId: 10, createdAt: '2026-01-02T00:00:00Z' },
];
const mockApiKeys: ApiKeyResponse[] = [
  { id: 1, name: 'CI Key', prefix: 'org-ci', isActive: true, expiresAt: null, createdAt: '2026-01-01T00:00:00Z' },
];
const mockPermissions: UserPermissionResponse[] = [
  { userId: 5, projectId: 10, isAdmin: false, permissions: ['ViewProject', 'EditFeature'] },
];

const dialogStub = { template: '<div><slot /></div>' };

function mountView() {
  const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: { template: '<div />' } }] });
  return mount(SettingsView, {
    global: { plugins: [router], stubs: { 'v-dialog': dialogStub } },
  });
}

describe('SettingsView', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
    jest.clearAllMocks();

    jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([]);
    jest.mocked(projectsApi.listProjectUserPermissions).mockResolvedValue([]);
    jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([]);
    jest.mocked(apiKeysApi.listApiKeys).mockResolvedValue([]);
    jest.mocked(webhooksApi.listOrgWebhooks).mockResolvedValue([]);
    jest.mocked(webhooksApi.listEnvWebhooks).mockResolvedValue([]);
  });

  describe('WhenNoOrganizationSelected', () => {
    it('ThenSelectOrgMessageIsShown', () => {
      const wrapper = mountView();
      expect(wrapper.text()).toContain('Select an organization');
    });
  });

  describe('WhenOrganizationIsSelected', () => {
    it('ThenTabsAreRendered', async () => {
      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);

      const wrapper = mountView();
      await flushPromises();

      expect(wrapper.find('[data-testid="tab-organization"]').exists()).toBe(true);
      expect(wrapper.find('[data-testid="tab-api-keys"]').exists()).toBe(true);
    });
  });

  // ── Organization tab ────────────────────────────────────────────────────────

  describe('WhenSaveOrgNameIsClicked', () => {
    it('ThenUpdateOrganizationIsCalledWithNewName', async () => {
      jest.mocked(orgsApi.updateOrganization).mockResolvedValue({ ...mockOrg, name: 'New Name' });

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);

      const wrapper = mountView();
      await flushPromises();

      await (wrapper.findComponent('[data-testid="org-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'New Name');
      await wrapper.find('[data-testid="save-org-name-btn"]').trigger('click');
      await flushPromises();

      expect(orgsApi.updateOrganization).toHaveBeenCalledWith(mockOrg.id, { name: 'New Name' });
    });
  });

  describe('WhenInviteMemberIsClicked', () => {
    it('ThenInviteOrganizationMembersByEmailIsCalledWithEmail', async () => {
      jest.mocked(orgsApi.inviteOrganizationMembersByEmail).mockResolvedValue([
        { email: 'jane@example.com', success: true, error: null },
      ]);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="add-member-btn"]').trigger('click');
      await wrapper.vm.$nextTick();

      await (wrapper.findComponent('[data-testid="add-member-email-0"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'jane@example.com');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="confirm-add-members-btn"]').trigger('click');
      await flushPromises();

      expect(orgsApi.inviteOrganizationMembersByEmail).toHaveBeenCalledWith(
        mockOrg.id,
        { invites: [{ email: 'jane@example.com', role: 'User' }] },
      );
    });
  });

  describe('WhenRemoveMemberIsClicked', () => {
    it('ThenRemoveOrganizationMemberIsCalledAndMemberIsRemovedFromList', async () => {
      jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([{ userId: 5, firstName: 'John', lastName: 'Smith', email: 'john@example.com', role: 'User', lastLoginAt: null }]);
      jest.mocked(orgsApi.removeOrganizationMember).mockResolvedValue(undefined);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);

      const wrapper = mountView();
      await flushPromises();

      expect(wrapper.find('[data-testid="member-row-5"]').exists()).toBe(true);

      await wrapper.find('[data-testid="remove-member-5"]').trigger('click');
      await wrapper.vm.$nextTick();
      await wrapper.find('[data-testid="confirm-delete-btn"]').trigger('click');
      await flushPromises();

      expect(orgsApi.removeOrganizationMember).toHaveBeenCalledWith(mockOrg.id, 5);
      expect(wrapper.find('[data-testid="member-row-5"]').exists()).toBe(false);
    });
  });

  // ── Project tab ─────────────────────────────────────────────────────────────

  describe('WhenSaveProjectIsClicked', () => {
    it('ThenUpdateProjectIsCalledWithFormValues', async () => {
      jest.mocked(projectsApi.updateProject).mockResolvedValue(mockProject);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-project"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="save-project-btn"]').trigger('click');
      await flushPromises();

      expect(projectsApi.updateProject).toHaveBeenCalledWith(
        mockProject.id,
        expect.objectContaining({ name: mockProject.name }),
      );
    });
  });

  describe('WhenPermissionsAreLoaded', () => {
    it('ThenPermissionRowsAreDisplayed', async () => {
      jest.mocked(projectsApi.listProjectUserPermissions).mockResolvedValue(mockPermissions);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-project"]').trigger('click');
      await wrapper.vm.$nextTick();

      expect(wrapper.find('[data-testid="permission-row-5"]').exists()).toBe(true);
    });
  });

  describe('WhenAdminToggleIsChanged', () => {
    it('ThenUpdateProjectUserPermissionsIsCalledWithIsAdmin', async () => {
      jest.mocked(projectsApi.listProjectUserPermissions).mockResolvedValue(mockPermissions);
      jest.mocked(projectsApi.updateProjectUserPermissions).mockResolvedValue({
        ...mockPermissions[0], isAdmin: true,
      });

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-project"]').trigger('click');
      await wrapper.vm.$nextTick();

      await (wrapper.findComponent(`[data-testid="perm-admin-toggle-5"]`) as VueWrapper<any>).vm.$emit('update:modelValue', true);
      await flushPromises();

      expect(projectsApi.updateProjectUserPermissions).toHaveBeenCalledWith(
        mockProject.id, 5,
        expect.objectContaining({ isAdmin: true }),
      );
    });
  });

  describe('WhenAddUserPermissionsIsSubmitted', () => {
    it('ThenSetProjectUserPermissionsIsCalledWithNewUserId', async () => {
      const newPerm: UserPermissionResponse = { userId: 99, projectId: 10, isAdmin: false, permissions: ['ViewProject'] };
      jest.mocked(projectsApi.setProjectUserPermissions).mockResolvedValue(newPerm);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-project"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="add-permission-btn"]').trigger('click');
      await wrapper.vm.$nextTick();

      await (wrapper.findComponent('[data-testid="new-perm-user-id-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', '99');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="save-new-permission-btn"]').trigger('click');
      await flushPromises();

      expect(projectsApi.setProjectUserPermissions).toHaveBeenCalledWith(
        mockProject.id,
        expect.objectContaining({ userId: 99 }),
      );
    });
  });

  describe('WhenRemovePermissionIsClicked', () => {
    it('ThenRemoveProjectUserPermissionsIsCalledAndRowIsRemoved', async () => {
      jest.mocked(projectsApi.listProjectUserPermissions).mockResolvedValue(mockPermissions);
      jest.mocked(projectsApi.removeProjectUserPermissions).mockResolvedValue(undefined);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-project"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="remove-permission-5"]').trigger('click');
      await wrapper.vm.$nextTick();
      await wrapper.find('[data-testid="confirm-delete-btn"]').trigger('click');
      await flushPromises();

      expect(projectsApi.removeProjectUserPermissions).toHaveBeenCalledWith(mockProject.id, 5);
      expect(wrapper.find('[data-testid="permission-row-5"]').exists()).toBe(false);
    });
  });

  // ── Environments tab ────────────────────────────────────────────────────────

  describe('WhenCreateEnvironmentIsClicked', () => {
    it('ThenCreateEnvironmentIsCalledAndEnvIsAddedToList', async () => {
      jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([]);
      jest.mocked(environmentsApi.createEnvironment).mockResolvedValue(mockEnvs[0]);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-environments"]').trigger('click');
      await wrapper.vm.$nextTick();

      await (wrapper.findComponent('[data-testid="new-env-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'Staging');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
      await flushPromises();

      expect(environmentsApi.createEnvironment).toHaveBeenCalledWith({ projectId: mockProject.id, name: 'Staging' });
    });
  });

  describe('WhenCloneEnvironmentIsConfirmed', () => {
    it('ThenCloneEnvironmentIsCalledWithNewName', async () => {
      jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([...mockEnvs]);
      jest.mocked(environmentsApi.cloneEnvironment).mockResolvedValue({
        ...mockEnvs[0], id: 300, name: 'Staging', apiKey: 'env-stg',
      });

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-environments"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="clone-env-100"]').trigger('click');
      await wrapper.vm.$nextTick();

      await (wrapper.findComponent('[data-testid="clone-env-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'Staging');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="confirm-clone-btn"]').trigger('click');
      await flushPromises();

      expect(environmentsApi.cloneEnvironment).toHaveBeenCalledWith(mockEnvs[0].apiKey, { name: 'Staging' });
    });
  });

  describe('WhenDeleteEnvironmentIsConfirmed', () => {
    it('ThenDeleteEnvironmentIsCalledAndEnvIsRemovedFromList', async () => {
      jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([...mockEnvs]);
      jest.mocked(environmentsApi.deleteEnvironment).mockResolvedValue(undefined);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-environments"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="delete-env-100"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="confirm-delete-env-btn"]').trigger('click');
      await flushPromises();

      expect(environmentsApi.deleteEnvironment).toHaveBeenCalledWith(mockEnvs[0].apiKey);
    });
  });

  describe('WhenRenameEnvironmentIsConfirmed', () => {
    it('ThenUpdateEnvironmentIsCalledWithNewName', async () => {
      jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([...mockEnvs]);
      jest.mocked(environmentsApi.updateEnvironment).mockResolvedValue({ ...mockEnvs[0], name: 'Dev Renamed' });

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);
      contextStore.setProject(mockProject);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-environments"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="rename-env-100"]').trigger('click');
      await wrapper.vm.$nextTick();

      await (wrapper.findComponent('[data-testid="rename-env-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'Dev Renamed');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="confirm-rename-btn"]').trigger('click');
      await flushPromises();

      expect(environmentsApi.updateEnvironment).toHaveBeenCalledWith(mockEnvs[0].apiKey, { name: 'Dev Renamed' });
    });
  });

  // ── API Keys tab ────────────────────────────────────────────────────────────

  describe('WhenApiKeysTabIsOpened', () => {
    it('ThenApiKeysAreLoaded', async () => {
      jest.mocked(apiKeysApi.listApiKeys).mockResolvedValue(mockApiKeys);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-api-keys"]').trigger('click');
      await wrapper.vm.$nextTick();

      expect(wrapper.find('[data-testid="api-key-row-1"]').exists()).toBe(true);
      expect(wrapper.text()).toContain('CI Key');
    });
  });

  describe('WhenCreateApiKeyIsSubmitted', () => {
    it('ThenCreateApiKeyIsCalledAndRawKeyDialogIsShown', async () => {
      const created: CreateApiKeyResponse = { id: 99, name: 'My Key', rawKey: 'raw-secret-key', prefix: 'org-my', expiresAt: null };
      jest.mocked(apiKeysApi.createApiKey).mockResolvedValue(created);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-api-keys"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="create-api-key-btn"]').trigger('click');
      await wrapper.vm.$nextTick();

      await (wrapper.findComponent('[data-testid="api-key-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'My Key');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="save-api-key-btn"]').trigger('click');
      await flushPromises();

      expect(apiKeysApi.createApiKey).toHaveBeenCalledWith(mockOrg.id, expect.objectContaining({ name: 'My Key' }));
      expect(wrapper.find('[data-testid="raw-key-dialog"]').exists()).toBe(true);
      expect(wrapper.find('[data-testid="raw-key-field"]').exists()).toBe(true);
    });

    it('ThenRawKeyIsDisplayedInTheRevealDialog', async () => {
      const created: CreateApiKeyResponse = { id: 99, name: 'My Key', rawKey: 'raw-secret-key', prefix: 'org-my', expiresAt: null };
      jest.mocked(apiKeysApi.createApiKey).mockResolvedValue(created);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-api-keys"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="create-api-key-btn"]').trigger('click');
      await wrapper.vm.$nextTick();

      await (wrapper.findComponent('[data-testid="api-key-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'My Key');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="save-api-key-btn"]').trigger('click');
      await flushPromises();

      const rawKeyField = wrapper.findComponent('[data-testid="raw-key-field"]') as VueWrapper<any>;
      expect(rawKeyField.props('modelValue')).toBe('raw-secret-key');
    });
  });

  describe('WhenDeleteApiKeyIsClicked', () => {
    it('ThenDeleteApiKeyIsCalledAndKeyIsRemovedFromList', async () => {
      jest.mocked(apiKeysApi.listApiKeys).mockResolvedValue([...mockApiKeys]);
      jest.mocked(apiKeysApi.deleteApiKey).mockResolvedValue(undefined);

      const contextStore = useContextStore();
      contextStore.setOrganization(mockOrg);

      const wrapper = mountView();
      await flushPromises();

      await wrapper.find('[data-testid="tab-api-keys"]').trigger('click');
      await wrapper.vm.$nextTick();

      await wrapper.find('[data-testid="delete-api-key-1"]').trigger('click');
      await wrapper.vm.$nextTick();
      await wrapper.find('[data-testid="confirm-delete-btn"]').trigger('click');
      await flushPromises();

      expect(apiKeysApi.deleteApiKey).toHaveBeenCalledWith(mockOrg.id, 1);
      expect(wrapper.find('[data-testid="api-key-row-1"]').exists()).toBe(false);
    });
  });
});
