import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useAccountCommunityAddresses } from '../use-account-community-addresses';
import { useAccountCommunitiesWithMetadata } from '../use-account-communities-with-metadata';
import useAuthorPrivileges from '../use-author-privileges';
import { useBoardFeedPageSize } from '../use-board-feed-page-size';
import { useBoardPseudonymityMode } from '../use-board-pseudonymity-mode';
import useCountLinksInReplies from '../use-count-links-in-replies';
import { useFilteredDirectoryAddresses } from '../use-filtered-directory-addresses';
import useAllFeedFilterStore from '../../stores/use-all-feed-filter-store';

(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;

const testState = vi.hoisted(() => ({
  account: undefined as unknown,
  accountCommunities: {} as Record<string, unknown>,
  directories: [] as Array<{ address: string; nsfw?: boolean }>,
  directoryLookup: {} as Record<string, unknown>,
  flattenedReplies: [] as unknown[],
  communitySnapshot: undefined as unknown,
}));

const accountsStoreSelectorCache = vi.hoisted(() => ({
  hasValue: false,
  value: undefined as unknown,
}));

vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
  useAccount: () => testState.account,
  useAccountCommunities: () => ({ accountCommunities: testState.accountCommunities }),
}));

vi.mock('@bitsocial/bitsocial-react-hooks/dist/stores/accounts/index.js', () => ({
  default: (
    selector: (state: { activeAccountId?: string; accounts: Record<string, { communities?: typeof testState.accountCommunities }> }) => unknown,
    equalityFn?: (previous: unknown, next: unknown) => boolean,
  ) => {
    const nextValue = selector({
      activeAccountId: 'active',
      accounts: {
        active: {
          communities: testState.accountCommunities,
        },
      },
    });

    if (accountsStoreSelectorCache.hasValue && equalityFn?.(accountsStoreSelectorCache.value, nextValue)) {
      return accountsStoreSelectorCache.value;
    }

    accountsStoreSelectorCache.hasValue = true;
    accountsStoreSelectorCache.value = nextValue;
    return nextValue;
  },
}));

vi.mock('@bitsocial/bitsocial-react-hooks/dist/lib/community-address.js', () => ({
  getEquivalentCommunityAddressGroupKey: (address: string) => (address.endsWith('.eth') ? address.slice(0, -4) + '.bso' : address),
  pickPreferredEquivalentCommunityAddress: (addresses: string[]) => addresses.find((address) => address.endsWith('.bso')) || addresses[0],
}));

vi.mock('@bitsocial/bitsocial-react-hooks/dist/lib/utils', () => ({
  flattenCommentsPages: () => testState.flattenedReplies,
}));

vi.mock('../use-directories', () => ({
  useDirectories: () => testState.directories,
  useDirectoryByAddress: (address: string | undefined) => (address ? testState.directoryLookup[address] : undefined),
}));

vi.mock('../use-stable-community', () => ({
  useCommunityField: (_address: string | undefined, selector: (community: unknown) => unknown) => selector(testState.communitySnapshot),
}));

let latestValue: unknown;
let root: Root;
let container: HTMLDivElement;
let renderCount = 0;

const HookHarness = ({ useValue }: { useValue: () => unknown }) => {
  const value = useValue();
  latestValue = value;
  return null;
};

const renderHookValue = (useValue: () => unknown) => {
  act(() => {
    root.render(createElement(HookHarness, { key: renderCount++, useValue }));
  });

  return latestValue;
};

const rerenderHookValue = (useValue: () => unknown) => {
  act(() => {
    root.render(createElement(HookHarness, { useValue }));
  });

  return latestValue;
};

describe('selector hooks', () => {
  beforeEach(() => {
    latestValue = undefined;
    renderCount = 0;
    localStorage.clear();
    vi.clearAllMocks();
    testState.account = undefined;
    testState.accountCommunities = {};
    testState.directories = [];
    testState.directoryLookup = {};
    testState.flattenedReplies = [];
    testState.communitySnapshot = undefined;
    accountsStoreSelectorCache.hasValue = false;
    accountsStoreSelectorCache.value = undefined;
    useAllFeedFilterStore.getState().setFilter('all');

    container = document.createElement('div');
    document.body.appendChild(container);
    root = createRoot(container);
  });

  afterEach(() => {
    act(() => root.unmount());
    container.remove();
  });

  it('derives account board addresses and metadata from cached account communities', () => {
    testState.accountCommunities = {
      'music.eth': { address: 'music.eth', title: '/mu/ - Music' },
      'tech.eth': { address: 'tech.eth', title: '/g/ - Technology' },
    };

    expect(renderHookValue(() => useAccountCommunityAddresses())).toEqual(['music.eth', 'tech.eth']);
    expect(renderHookValue(() => useAccountCommunitiesWithMetadata())).toEqual([
      { address: 'music.eth', title: '/mu/ - Music' },
      { address: 'tech.eth', title: '/g/ - Technology' },
    ]);
  });

  it('keeps account board address identity stable when cached community objects change', () => {
    testState.accountCommunities = {
      'music.eth': { address: 'music.eth', state: 'updating' },
      'tech.eth': { address: 'tech.eth', state: 'updating' },
    };

    const initialAddresses = rerenderHookValue(() => useAccountCommunityAddresses());

    testState.accountCommunities = {
      'music.eth': { address: 'music.eth', state: 'succeeded' },
      'tech.eth': { address: 'tech.eth', state: 'updating' },
    };

    expect(rerenderHookValue(() => useAccountCommunityAddresses())).toBe(initialAddresses);

    testState.accountCommunities = {
      ...testState.accountCommunities,
      'biz.eth': { address: 'biz.eth', state: 'updating' },
    };

    const addressesWithNewBoard = rerenderHookValue(() => useAccountCommunityAddresses());
    expect(addressesWithNewBoard).not.toBe(initialAddresses);
    expect(addressesWithNewBoard).toEqual(['biz.eth', 'music.eth', 'tech.eth']);
  });

  it('computes moderator privileges and whether the current account authored the comment', () => {
    testState.account = { author: { address: '0xme' } };
    testState.communitySnapshot = {
      roles: {
        '0xauthor': { role: 'moderator' },
        '0xme': { role: 'admin' },
      },
    };

    expect(renderHookValue(() => useAuthorPrivileges({ commentAuthorAddress: '0xauthor', communityAddress: 'music.eth' }))).toEqual({
      isCommentAuthorMod: true,
      isAccountMod: true,
      isAccountCommentAuthor: false,
      commentAuthorRole: 'moderator',
      accountAuthorRole: 'admin',
    });

    expect(renderHookValue(() => useAuthorPrivileges({ commentAuthorAddress: '0xme', communityAddress: 'music.eth' }))).toEqual({
      isCommentAuthorMod: true,
      isAccountMod: true,
      isAccountCommentAuthor: true,
      commentAuthorRole: 'admin',
      accountAuthorRole: 'admin',
    });
  });

  it('derives board page sizes from directory metadata and falls back when unavailable', () => {
    expect(renderHookValue(() => useBoardFeedPageSize({ features: { postsPerPage: 22 } } as never))).toEqual({
      guiPostsPerPage: 22,
      maxGuiPages: 10,
      paginationFeedPostsPerPage: 220,
      infiniteFeedPostsPerPage: 22,
    });

    expect(renderHookValue(() => useBoardFeedPageSize(undefined))).toEqual({
      guiPostsPerPage: 15,
      maxGuiPages: 10,
      paginationFeedPostsPerPage: 150,
      infiniteFeedPostsPerPage: 15,
    });
  });

  it('prefers live pseudonymity metadata and falls back to directory entries', () => {
    testState.directoryLookup = {
      'music.eth': {
        address: 'music.eth',
        features: { pseudonymityMode: 'directory-mode' },
      },
    };
    testState.communitySnapshot = {
      features: { pseudonymityMode: 'live-mode' },
    };

    expect(renderHookValue(() => useBoardPseudonymityMode('music.eth'))).toBe('live-mode');

    testState.communitySnapshot = {
      features: {},
    };

    expect(renderHookValue(() => useBoardPseudonymityMode('music.eth'))).toBe('directory-mode');
  });

  it('counts link-bearing replies and supports a reply preview limit', () => {
    testState.flattenedReplies = [{ link: 'https://a.test' }, { link: undefined }, { link: 'https://b.test' }];

    expect(renderHookValue(() => useCountLinksInReplies({ replies: {} } as never))).toBe(2);
    expect(renderHookValue(() => useCountLinksInReplies({ replies: {} } as never, 2))).toBe(1);
  });

  it('filters directory addresses according to the all-feed mode', () => {
    testState.directories = [{ address: 'music.eth', nsfw: false }, { address: 'flash.eth', nsfw: true }, { address: 'tech.eth' }];

    expect(renderHookValue(() => useFilteredDirectoryAddresses())).toEqual(['music.eth', 'flash.eth', 'tech.eth']);

    act(() => {
      useAllFeedFilterStore.getState().setFilter('nsfw');
    });
    expect(renderHookValue(() => useFilteredDirectoryAddresses())).toEqual(['flash.eth']);

    act(() => {
      useAllFeedFilterStore.getState().setFilter('sfw');
    });
    expect(renderHookValue(() => useFilteredDirectoryAddresses())).toEqual(['music.eth', 'tech.eth']);
  });
});
