import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of, firstValueFrom } from "rxjs";

import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { newGuid } from "@bitwarden/guid";

import { BasePolicyEditDefinition } from "./base-policy-edit.component";
import { PolicyCategory } from "./pipes/policy-category";
import { PoliciesComponent } from "./policies.component";
import { SingleOrgPolicy } from "./policy-edit-definitions/single-org.component";
import { PolicyEditDialogComponent } from "./policy-edit-dialog.component";
import { PolicyListService } from "./policy-list.service";
import { POLICY_EDIT_REGISTER } from "./policy-register-token";

describe("PoliciesComponent", () => {
  let component: PoliciesComponent;
  let fixture: ComponentFixture<PoliciesComponent>;

  let mockActivatedRoute: ActivatedRoute;
  let mockOrganizationService: MockProxy<OrganizationService>;
  let mockAccountService: FakeAccountService;
  let mockPolicyApiService: MockProxy<PolicyApiServiceAbstraction>;
  let mockPolicyListService: MockProxy<PolicyListService>;
  let mockDialogService: MockProxy<DialogService>;
  let mockPolicyService: MockProxy<PolicyService>;
  let mockConfigService: MockProxy<ConfigService>;
  let mockI18nService: MockProxy<I18nService>;
  let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;

  let routeParamsSubject: BehaviorSubject<any>;
  let queryParamsSubject: BehaviorSubject<any>;

  const mockUserId = newGuid() as UserId;
  const mockOrgId = newGuid() as OrganizationId;
  const mockOrg = {
    id: mockOrgId,
    name: "Test Organization",
    enabled: true,
  } as Organization;

  const mockPolicyResponse: {
    id: string;
    enabled: boolean;
    object: string;
    organizationId: string;
    type: PolicyType;
    data: null;
  } = {
    id: newGuid(),
    enabled: true,
    object: "policy",
    organizationId: mockOrgId,
    type: PolicyType.SingleOrg,
    data: null,
  };

  const mockPolicy = new SingleOrgPolicy();

  beforeEach(async () => {
    routeParamsSubject = new BehaviorSubject({ organizationId: mockOrgId });
    queryParamsSubject = new BehaviorSubject({});

    mockActivatedRoute = {
      params: routeParamsSubject.asObservable(),
      queryParams: queryParamsSubject.asObservable(),
    } as any;

    mockOrganizationService = mock<OrganizationService>();
    mockOrganizationService.organizations$.mockReturnValue(of([mockOrg]));

    mockAccountService = mockAccountServiceWith(mockUserId);

    mockPolicyApiService = mock<PolicyApiServiceAbstraction>();
    mockPolicyApiService.getPolicies.mockResolvedValue(
      new ListResponse({ Data: [mockPolicyResponse], ContinuationToken: null }, PolicyResponse),
    );

    mockPolicyListService = mock<PolicyListService>();
    mockPolicyListService.getPolicies.mockReturnValue([mockPolicy]);
    (mockPolicyListService as any).sections = [];

    mockDialogService = mock<DialogService>();
    mockDialogService.open.mockReturnValue({ close: jest.fn() } as any);

    mockPolicyService = mock<PolicyService>();
    mockPolicyService.policies$.mockReturnValue(of([]));

    mockConfigService = mock<ConfigService>();
    mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
    mockI18nService = mock<I18nService>();
    mockPlatformUtilsService = mock<PlatformUtilsService>();

    jest.spyOn(PolicyEditDialogComponent, "open").mockReturnValue({ close: jest.fn() } as any);

    await TestBed.configureTestingModule({
      imports: [PoliciesComponent],
      providers: [
        { provide: ActivatedRoute, useValue: mockActivatedRoute },
        { provide: OrganizationService, useValue: mockOrganizationService },
        { provide: AccountService, useValue: mockAccountService },
        { provide: PolicyApiServiceAbstraction, useValue: mockPolicyApiService },
        { provide: PolicyListService, useValue: mockPolicyListService },
        { provide: DialogService, useValue: mockDialogService },
        { provide: PolicyService, useValue: mockPolicyService },
        { provide: ConfigService, useValue: mockConfigService },
        { provide: I18nService, useValue: mockI18nService },
        { provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
        { provide: POLICY_EDIT_REGISTER, useValue: [] },
      ],
      schemas: [NO_ERRORS_SCHEMA],
    })
      .overrideComponent(PoliciesComponent, {
        remove: { imports: [] },
        add: { template: "<div></div>" },
      })
      .compileComponents();

    fixture = TestBed.createComponent(PoliciesComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  afterEach(() => {
    if (fixture) {
      fixture.destroy();
    }
    jest.restoreAllMocks();
  });

  it("should create", () => {
    expect(component).toBeTruthy();
  });

  describe("organizationId$", () => {
    it("should extract organizationId from route params", async () => {
      const orgId = await firstValueFrom(component["organizationId$"]);
      expect(orgId).toBe(mockOrgId);
    });

    it("should emit new organizationId when route params change", (done) => {
      const newOrgId = newGuid() as OrganizationId;
      const emittedValues: OrganizationId[] = [];

      const subscription = component["organizationId$"].subscribe((orgId) => {
        emittedValues.push(orgId);

        if (emittedValues.length === 2) {
          expect(emittedValues[0]).toBe(mockOrgId);
          expect(emittedValues[1]).toBe(newOrgId);
          subscription.unsubscribe();
          done();
        }
      });

      routeParamsSubject.next({ organizationId: newOrgId });
    });
  });

  describe("organization$", () => {
    it("should retrieve organization for current user and organizationId", async () => {
      const org = await firstValueFrom(component["organization$"]);
      expect(org).toBe(mockOrg);
      expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId);
    });

    it("should throw error when organization is not found", async () => {
      mockOrganizationService.organizations$.mockReturnValue(of([]));

      await expect(firstValueFrom(component["organization$"])).rejects.toThrow(
        "No organization found for provided userId",
      );
    });
  });

  describe("policySections$", () => {
    it("should return sections from PolicyListService", async () => {
      const sections = await firstValueFrom(component["policySections$"]);

      expect(sections).toBeDefined();
      expect(Array.isArray(sections)).toBe(true);
    });
  });

  describe("orgPolicies$", () => {
    describe("with multiple policies", () => {
      const mockPolicyResponsesData: {
        id: string;
        organizationId: string;
        type: PolicyType;
        enabled: boolean;
        data: null;
      }[] = [
        {
          id: newGuid(),
          organizationId: mockOrgId,
          type: PolicyType.TwoFactorAuthentication,
          enabled: true,
          data: null,
        },
        {
          id: newGuid(),
          organizationId: mockOrgId,
          type: PolicyType.RequireSso,
          enabled: false,
          data: null,
        },
      ];

      beforeEach(async () => {
        const listResponse = new ListResponse(
          { Data: mockPolicyResponsesData, ContinuationToken: null },
          PolicyResponse,
        );

        mockPolicyApiService.getPolicies.mockResolvedValue(listResponse);

        fixture = TestBed.createComponent(PoliciesComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });

      it("should fetch policies from API for current organization", async () => {
        const policies = await firstValueFrom(component["orgPolicies$"]);
        expect(policies.length).toBe(2);
        expect(mockPolicyApiService.getPolicies).toHaveBeenCalledWith(mockOrgId);
      });
    });

    describe("with no policies", () => {
      beforeEach(async () => {
        mockPolicyApiService.getPolicies.mockResolvedValue(
          new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse),
        );

        fixture = TestBed.createComponent(PoliciesComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });

      it("should return empty array when API returns no data", async () => {
        const policies = await firstValueFrom(component["orgPolicies$"]);
        expect(policies).toEqual([]);
      });
    });

    describe("with null data", () => {
      beforeEach(async () => {
        mockPolicyApiService.getPolicies.mockResolvedValue(
          new ListResponse({ Data: null, ContinuationToken: null }, PolicyResponse),
        );

        fixture = TestBed.createComponent(PoliciesComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });

      it("should return empty array when API returns null data", async () => {
        const policies = await firstValueFrom(component["orgPolicies$"]);
        expect(policies).toEqual([]);
      });
    });
  });

  describe("policiesEnabledMap$", () => {
    describe("with multiple policies", () => {
      const mockPolicyResponsesData: {
        id: string;
        organizationId: string;
        type: PolicyType;
        enabled: boolean;
        data: null;
      }[] = [
        {
          id: "policy-1",
          organizationId: mockOrgId,
          type: PolicyType.TwoFactorAuthentication,
          enabled: true,
          data: null,
        },
        {
          id: "policy-2",
          organizationId: mockOrgId,
          type: PolicyType.RequireSso,
          enabled: false,
          data: null,
        },
        {
          id: "policy-3",
          organizationId: mockOrgId,
          type: PolicyType.SingleOrg,
          enabled: true,
          data: null,
        },
      ];

      beforeEach(async () => {
        mockPolicyApiService.getPolicies.mockResolvedValue(
          new ListResponse(
            { Data: mockPolicyResponsesData, ContinuationToken: null },
            PolicyResponse,
          ),
        );

        fixture = TestBed.createComponent(PoliciesComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });

      it("should create a map of policy types to their enabled status", async () => {
        const map = await firstValueFrom(component["policiesEnabledMap$"]);
        expect(map.size).toBe(3);
        expect(map.get(PolicyType.TwoFactorAuthentication)).toBe(true);
        expect(map.get(PolicyType.RequireSso)).toBe(false);
        expect(map.get(PolicyType.SingleOrg)).toBe(true);
      });
    });

    describe("with no policies", () => {
      beforeEach(async () => {
        mockPolicyApiService.getPolicies.mockResolvedValue(
          new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse),
        );

        fixture = TestBed.createComponent(PoliciesComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });

      it("should create empty map when no policies exist", async () => {
        const map = await firstValueFrom(component["policiesEnabledMap$"]);
        expect(map.size).toBe(0);
      });
    });
  });

  describe("constructor subscription", () => {
    it("should subscribe to policyService.policies$ on initialization", () => {
      expect(mockPolicyService.policies$).toHaveBeenCalledWith(mockUserId);
    });

    describe("when policyService emits", () => {
      let policiesSubject: BehaviorSubject<any[]>;
      let callCount: number;

      beforeEach(async () => {
        policiesSubject = new BehaviorSubject<any[]>([]);
        mockPolicyService.policies$.mockReturnValue(policiesSubject.asObservable());

        callCount = 0;
        mockPolicyApiService.getPolicies.mockImplementation(async () => {
          callCount++;
          return new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse);
        });

        fixture = TestBed.createComponent(PoliciesComponent);
        fixture.detectChanges();
      });

      it("should refresh policies when policyService emits", () => {
        const initialCallCount = callCount;

        policiesSubject.next([{ type: PolicyType.TwoFactorAuthentication }]);

        expect(callCount).toBeGreaterThan(initialCallCount);
      });
    });
  });

  describe("handleLaunchEvent", () => {
    describe("when policyId is in query params", () => {
      const mockPolicyId = newGuid();
      const mockPolicy: BasePolicyEditDefinition = {
        name: "Test Policy",
        description: "Test Description",
        type: PolicyType.TwoFactorAuthentication,
        category: PolicyCategory.Authentication,
        priority: 10,
        component: {} as any,
        showDescription: true,
        showEnabledBadge: false,
        display$: () => of(true),
      };

      const mockPolicyResponseData: {
        id: string;
        organizationId: string;
        type: PolicyType;
        enabled: boolean;
        data: null;
      } = {
        id: mockPolicyId,
        organizationId: mockOrgId,
        type: PolicyType.TwoFactorAuthentication,
        enabled: true,
        data: null,
      };

      let dialogOpenSpy: jest.SpyInstance;

      beforeEach(async () => {
        queryParamsSubject.next({ policyId: mockPolicyId });

        mockPolicyApiService.getPolicies.mockResolvedValue(
          new ListResponse(
            { Data: [mockPolicyResponseData], ContinuationToken: null },
            PolicyResponse,
          ),
        );

        dialogOpenSpy = jest
          .spyOn(PolicyEditDialogComponent, "open")
          .mockReturnValue({ close: jest.fn() } as any);

        TestBed.resetTestingModule();
        await TestBed.configureTestingModule({
          imports: [PoliciesComponent],
          providers: [
            { provide: ActivatedRoute, useValue: mockActivatedRoute },
            { provide: OrganizationService, useValue: mockOrganizationService },
            { provide: AccountService, useValue: mockAccountService },
            { provide: PolicyApiServiceAbstraction, useValue: mockPolicyApiService },
            { provide: PolicyListService, useValue: mockPolicyListService },
            { provide: DialogService, useValue: mockDialogService },
            { provide: PolicyService, useValue: mockPolicyService },
            { provide: ConfigService, useValue: mockConfigService },
            { provide: I18nService, useValue: mockI18nService },
            { provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
            { provide: POLICY_EDIT_REGISTER, useValue: [mockPolicy] },
          ],
          schemas: [NO_ERRORS_SCHEMA],
        })
          .overrideComponent(PoliciesComponent, {
            remove: { imports: [] },
            add: { template: "<div></div>" },
          })
          .compileComponents();

        fixture = TestBed.createComponent(PoliciesComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });

      it("should open policy dialog when policyId is in query params", () => {
        expect(dialogOpenSpy).toHaveBeenCalled();
        const callArgs = dialogOpenSpy.mock.calls[0][1];
        expect(callArgs.data?.policy.type).toBe(mockPolicy.type);
        expect(callArgs.data?.organization).toBe(mockOrg);
      });
    });

    it("should not open dialog when policyId is not in query params", async () => {
      const editSpy = jest.spyOn(component, "edit");

      queryParamsSubject.next({});

      expect(editSpy).not.toHaveBeenCalled();
    });

    it("should not open dialog when policyId does not match any org policy", async () => {
      const mockPolicy: BasePolicyEditDefinition = {
        name: "Test Policy",
        description: "Test Description",
        type: PolicyType.TwoFactorAuthentication,
        category: PolicyCategory.Authentication,
        priority: 10,
        component: {} as any,
        showDescription: true,
        showEnabledBadge: false,
        display$: () => of(true),
      };

      mockPolicyListService.getPolicies.mockReturnValue([mockPolicy]);
      mockPolicyApiService.getPolicies.mockResolvedValue(
        new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse),
      );

      const editSpy = jest.spyOn(component, "edit");

      queryParamsSubject.next({ policyId: "non-existent-policy-id" });

      expect(editSpy).not.toHaveBeenCalled();
    });
  });

  describe("edit", () => {
    it("should call dialogService.open with correct parameters when no custom dialog is specified", async () => {
      const mockPolicy: BasePolicyEditDefinition = {
        name: "Test Policy",
        description: "Test Description",
        type: PolicyType.TwoFactorAuthentication,
        category: PolicyCategory.Authentication,
        priority: 10,
        component: {} as any,
        showDescription: true,
        showEnabledBadge: false,
        display$: () => of(true),
      };

      const openSpy = jest.spyOn(PolicyEditDialogComponent, "open");

      await component.edit(mockPolicy, mockOrg);

      expect(openSpy).toHaveBeenCalled();
      const callArgs = openSpy.mock.calls[0];
      expect(callArgs[1]).toEqual({
        data: {
          policy: mockPolicy,
          organization: mockOrg,
        },
      });
    });

    it("should call custom dialog open method when specified", async () => {
      const mockDialogRef = { close: jest.fn() };
      const mockCustomDialog = {
        open: jest.fn().mockReturnValue(mockDialogRef),
      };

      const mockPolicy: BasePolicyEditDefinition = {
        name: "Custom Policy",
        description: "Custom Description",
        type: PolicyType.RequireSso,
        category: PolicyCategory.Authentication,
        priority: 10,
        component: {} as any,
        editDialogComponent: mockCustomDialog as any,
        showDescription: true,
        showEnabledBadge: false,
        display$: () => of(true),
      };

      await component.edit(mockPolicy, mockOrg);

      expect(mockCustomDialog.open).toHaveBeenCalled();
      const callArgs = mockCustomDialog.open.mock.calls[0];
      expect(callArgs[1]).toEqual({
        data: {
          policy: mockPolicy,
          organization: mockOrg,
        },
      });
      expect(PolicyEditDialogComponent.open).not.toHaveBeenCalled();
    });

    it("should pass organization to dialog", async () => {
      const customOrg = { id: newGuid() as OrganizationId, name: "Custom Org" } as Organization;
      const mockPolicy: BasePolicyEditDefinition = {
        name: "Test Policy",
        description: "Test Description",
        type: PolicyType.SingleOrg,
        category: PolicyCategory.Authentication,
        priority: 10,
        component: {} as any,
        showDescription: true,
        showEnabledBadge: false,
        display$: () => of(true),
      };

      const openSpy = jest.spyOn(PolicyEditDialogComponent, "open");

      await component.edit(mockPolicy, customOrg);

      expect(openSpy).toHaveBeenCalled();
      const callArgs = openSpy.mock.calls[0];
      expect(callArgs[1]).toEqual({
        data: {
          policy: mockPolicy,
          organization: customOrg,
        },
      });
    });

    it("should open drawer when PolicyDrawers flag is enabled and openDrawer is present", async () => {
      mockConfigService.getFeatureFlag$.mockReturnValue(of(true));

      fixture = TestBed.createComponent(PoliciesComponent);
      component = fixture.componentInstance;
      fixture.detectChanges();

      const mockDrawerRef = { close: jest.fn(), closed: of(undefined) };
      const mockDrawerDialog = {
        open: jest.fn(),
        openDrawer: jest.fn().mockReturnValue(mockDrawerRef),
      };

      const mockPolicy: BasePolicyEditDefinition = {
        name: "Drawer Policy",
        description: "Drawer Description",
        type: PolicyType.TwoFactorAuthentication,
        category: PolicyCategory.Authentication,
        priority: 10,
        component: {} as any,
        editDialogComponent: mockDrawerDialog as any,
        showDescription: true,
        showEnabledBadge: false,
        display$: () => of(true),
      };

      await component.edit(mockPolicy, mockOrg);

      expect(mockDrawerDialog.openDrawer).toHaveBeenCalled();
      const callArgs = mockDrawerDialog.openDrawer.mock.calls[0];
      expect(callArgs[1]).toEqual({
        data: {
          policy: mockPolicy,
          organization: mockOrg,
        },
      });
      expect(mockDrawerDialog.open).not.toHaveBeenCalled();
    });

    it("clears the drawer ref once it closes, so canDeactivate doesn't re-close a stale ref", async () => {
      mockConfigService.getFeatureFlag$.mockReturnValue(of(true));

      fixture = TestBed.createComponent(PoliciesComponent);
      component = fixture.componentInstance;
      fixture.detectChanges();

      // Simulate the real DrawerRef behavior: calling close() on an already-closed ref
      // short-circuits to `{ closed: false }`. If the component failed to clear its
      // `drawerRef` signal after the drawer closed, canDeactivate() would call this again
      // and incorrectly block navigation.
      const closeSpy = jest.fn().mockResolvedValue({ closed: false });
      const mockDrawerRef = { close: closeSpy, closed: of(undefined) };
      const mockDrawerDialog = {
        open: jest.fn(),
        openDrawer: jest.fn().mockReturnValue(mockDrawerRef),
      };

      const mockPolicy: BasePolicyEditDefinition = {
        name: "Drawer Policy",
        description: "Drawer Description",
        type: PolicyType.TwoFactorAuthentication,
        category: PolicyCategory.Authentication,
        priority: 10,
        component: {} as any,
        editDialogComponent: mockDrawerDialog as any,
        showDescription: true,
        showEnabledBadge: false,
        display$: () => of(true),
      };

      // The drawer's `closed` observable (of(undefined)) completes synchronously once
      // subscribed, simulating a save/cancel that already closed the drawer.
      await component.edit(mockPolicy, mockOrg);

      const canLeave = await component.canDeactivate();

      expect(canLeave).toBe(true);
      expect(closeSpy).not.toHaveBeenCalled();
    });
  });
});
