/**
 * Copyright (c) 2026 Huawei Technologies Co., Ltd.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree.
 */

import { Autolinking, AutolinkingConfig } from './Autolinking';
import { NestedDirectoryJSON } from 'memfs';
import { AbsolutePath, DescriptiveError } from '../core/';
import { MockedLogger, MemFS } from '../io/__fixtures__';
import pathUtils from 'node:path';

function createAutolinking({
  fsStructure,
}: {
  fsStructure: NestedDirectoryJSON;
}) {
  const memFS = new MemFS(fsStructure);
  const mockedLogger = new MockedLogger();
  const autolinking = new Autolinking(memFS, mockedLogger);

  return {
    runAutolinking: async (config: Partial<AutolinkingConfig> = {}) => {
      const input = await autolinking.prepareInput({
        harmonyProjectPath:
          config.harmonyProjectPath ?? new AbsolutePath('./harmony'),
        nodeModulesPath:
          config.nodeModulesPath ?? new AbsolutePath('./node_modules'),
        cppRNOHPackagesFactoryPathRelativeToHarmony:
          config.cppRNOHPackagesFactoryPathRelativeToHarmony ??
          './entry/src/main/cpp/RNOHPackageFactory.h',
        ohPackagePathRelativeToHarmony:
          config.ohPackagePathRelativeToHarmony ?? './oh-package.json5',
        etsRNOHPackagesFactoryPathRelativeToHarmony:
          config.etsRNOHPackagesFactoryPathRelativeToHarmony ??
          './entry/src/main/ets/RNOHPackageFactory.ets',
        cmakeAutolinkPathRelativeToHarmony:
          config.cmakeAutolinkPathRelativeToHarmony ??
          './entry/src/main/cpp/autolink.cmake',
        excludedNpmPackageNames: config.excludedNpmPackageNames ?? new Set(),
        includedNpmPackageNames: config.includedNpmPackageNames ?? new Set(),
      });
      const output = autolinking.evaluate(input);
      autolinking.saveAndLogOutput(output);
      return {
        cmakeAutolinkingPath: output.cmakeAutolinkingPathAndContent[0],
        cppRNOHPackagesFactoryPath:
          output.cppRNOHPackagesFactoryPathAndContent[0],
        etsRNOHPackagesFactoryPath:
          output.etsRNOHPackagesFactoryPathAndContent[0],
        ohPackagePath: output.ohPackagePathAndContent[0],
        logs: mockedLogger.getLogs(),
      };
    },
    memFS,
  };
}

const baseFileStructure = {
  harmony: {
    entry: {
      src: {
        main: {
          ets: {},
          cpp: {},
        },
      },
    },
    'oh-package.json5': `{
      "dependencies": {
      }
    }`,
  },
};

it('should generate correct templates with scoped package default configuration', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        '@rnoh': {
          'link-scoped': {
            harmony: {
              'link_scoped.har': '',
            },
            'package.json': JSON.stringify({
              name: '@rnoh/link-scoped',
              harmony: {
                autolinking: true,
              },
            }),
          },
        },
      },
    },
  });

  await runAutolinking();

  const ohPackageContent = memFS.readTextSync(
    new AbsolutePath('./harmony/oh-package.json5')
  );
  expect(ohPackageContent).toContain('dependencies');
  expect(ohPackageContent).toContain('@rnoh/rnoh--link-scoped');
  expect(ohPackageContent).toContain(
    'file:../node_modules/@rnoh/link-scoped/harmony/link_scoped.har'
  );
  expect(
    memFS.readTextSync(
      new AbsolutePath('./harmony/entry/src/main/ets/RNOHPackageFactory.ets')
    )
  ).toBe(
    `
/*
 * This file was generated by RNOH autolinking.
 * DO NOT modify it manually, your changes WILL be overwritten.
 */
import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony';
import RnohLinkScopedPackage from '@rnoh/rnoh--link-scoped';

export function createRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
  return [
    new RnohLinkScopedPackage(ctx),
  ];
}
`.trimStart()
  );

  expect(
    memFS.readTextSync(
      new AbsolutePath('./harmony/entry/src/main/cpp/RNOHPackageFactory.h')
    )
  ).toBe(
    `
/*
 * This file was generated by RNOH autolinking.
 * DO NOT modify it manually, your changes WILL be overwritten.
 */
// clang-format off
#pragma once
#include "RNOH/Package.h"
#include "RnohLinkScopedPackage.h"

std::vector<rnoh::Package::Shared> createRNOHPackages(const rnoh::Package::Context &ctx) {
  return {
    std::make_shared<rnoh::RnohLinkScopedPackage>(ctx),
  };
}
`.trimStart()
  );

  expect(
    memFS.readTextSync(
      new AbsolutePath('./harmony/entry/src/main/cpp/autolink.cmake')
    )
  ).toBe(
    `
# This file was generated by RNOH autolinking.
# DO NOT modify it manually, your changes WILL be overwritten.
cmake_minimum_required(VERSION 3.5)

# @actor RNOH_APP
function(autolink_libraries target)
    add_subdirectory("\${OH_MODULES_DIR}/@rnoh/rnoh--link-scoped/src/main/cpp" ./rnoh__rnoh__link_scoped)

    set(AUTOLINKED_LIBRARIES
        rnoh__rnoh__link_scoped
    )

    foreach(lib \${AUTOLINKED_LIBRARIES})
        target_link_libraries(\${target} PUBLIC \${lib})
    endforeach()
endfunction()
`.trimStart()
  );
});

it('should handle custom autolinking configuration', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        '@rnoh': {
          'custom-config': {
            harmony: {
              'custom_config.har': '',
            },
            'package.json': JSON.stringify({
              name: '@rnoh/custom-config',
              harmony: {
                autolinking: {
                  ohPackageName: '@rnoh/custom-oh-name',
                  etsPackageClassName: 'CustomEtsClass',
                  cppPackageClassName: 'CustomCppClass',
                  cmakeLibraryTargetName: 'custom_cmake_target',
                },
              },
            }),
          },
        },
      },
    },
  });

  const output = await runAutolinking();

  expect(memFS.readTextSync(output.etsRNOHPackagesFactoryPath)).toContain(
    'CustomEtsClass'
  );
  expect(memFS.readTextSync(output.etsRNOHPackagesFactoryPath)).not.toContain(
    'CustomCppClass'
  );
  expect(memFS.readTextSync(output.cppRNOHPackagesFactoryPath)).toContain(
    'CustomCppClass'
  );
  expect(memFS.readTextSync(output.cppRNOHPackagesFactoryPath)).not.toContain(
    'CustomEtsClass'
  );
  expect(memFS.readTextSync(output.cmakeAutolinkingPath)).toContain(
    'custom_cmake_target'
  );
  expect(memFS.readTextSync(output.ohPackagePath)).toContain(
    '@rnoh/custom-oh-name'
  );
});

it('should handle unscoped package correctly', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'unscoped-package': {
          harmony: {
            'unscoped_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'unscoped-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
    },
  });

  const output = await runAutolinking();

  expect(memFS.readTextSync(output.etsRNOHPackagesFactoryPath)).toContain(
    'UnscopedPackage'
  );
  expect(memFS.readTextSync(output.cppRNOHPackagesFactoryPath)).toContain(
    'UnscopedPackage'
  );
  expect(memFS.readTextSync(output.ohPackagePath)).toContain(
    '@rnoh/unscoped-package'
  );
});

it('should remove entries to missing hars from oh-package.json5 and preserve custom entries not managed by autolinking', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        '@rnoh': {
          'existing-har': {
            harmony: {
              'existing_har.har': '',
            },
            'package.json': JSON.stringify({
              name: '@rnoh/existing-har',
              harmony: {
                autolinking: null,
              },
            }),
          },
          'missing-har': {
            harmony: {
              // No .har file here
            },
            'package.json': JSON.stringify({
              name: '@rnoh/missing-har',
            }),
          },
        },
      },
      harmony: {
        ...baseFileStructure.harmony,
        'oh-package.json5': `{
            "dependencies": {
              "@rnoh/existing-har": "file:../node_modules/@rnoh/existing-har/harmony/existing_har.har",
              "@rnoh/missing-har": "file:../node_modules/@rnoh/missing-har/harmony/missing_har.har"
            }
          }`,
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  expect(ohPackageContent).toContain('@rnoh/existing-har');
  expect(ohPackageContent).not.toContain('@rnoh/missing-har');
});

it('should log updated files', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'unscoped-package': {
          harmony: {
            'unscoped_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'unscoped-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
    },
  });

  const { logs } = await runAutolinking();

  const combinedLogs = logs.map((log) => log.msg).join('\n');
  expect(combinedLogs).toContain(
    pathUtils.normalize('harmony/entry/src/main/cpp/autolink.cmake')
  );
  expect(combinedLogs).toContain(
    pathUtils.normalize('harmony/entry/src/main/ets/RNOHPackageFactory.ets')
  );
  expect(combinedLogs).toContain(
    pathUtils.normalize('harmony/entry/src/main/cpp/RNOHPackageFactory.h')
  );
  expect(combinedLogs).toContain(
    pathUtils.normalize('harmony/oh-package.json5')
  );
});

it('should log linked and skipped packages', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'autolinkable-package': {
          harmony: {
            'autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'autolinkable-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
        'ignored-package': {
          harmony: {
            'ignored_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'ignored-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
    },
  });

  const { logs } = await runAutolinking({
    excludedNpmPackageNames: new Set<string>().add('ignored-package'),
  });

  const combinedLogs = logs.map((log) => log.msg).join('\n');
  expect(combinedLogs).toContain('[link] autolinkable-package');
  expect(combinedLogs).toContain('[skip] ignored-package');
});

it('should fail when user lists included and excluded packages', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'autolinkable-package': {
          harmony: {
            'autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'autolinkable-package',
          }),
        },
        'ignored-package': {
          harmony: {
            'ignored_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'ignored-package',
          }),
        },
      },
    },
  });

  expect(() =>
    runAutolinking({
      includedNpmPackageNames: new Set<string>().add('autolinkable-package'),
      excludedNpmPackageNames: new Set<string>().add('autolinkable-package'),
    })
  ).rejects.toThrow(DescriptiveError);
});

it('should link only specified packages', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'autolinkable-package': {
          harmony: {
            'autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'autolinkable-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
        'ignored-package': {
          harmony: {
            'ignored_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'ignored-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
    },
  });

  const { logs } = await runAutolinking({
    includedNpmPackageNames: new Set<string>().add('autolinkable-package'),
  });

  const combinedLogs = logs.map((log) => log.msg).join('\n');
  expect(combinedLogs).toContain('[link] autolinkable-package');
  expect(combinedLogs).toContain('[skip] ignored-package');
});

it('should link by default only those packages that support autolinking', async () => {
  const { runAutolinking } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'autolinkable-package': {
          harmony: {
            'autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'autolinkable-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
        'not-autolinkable-package': {
          harmony: {
            'not_autolinkable_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'not-autolinkable-package',
          }),
        },
      },
    },
  });

  const { logs } = await runAutolinking({});

  const combinedLogs = logs.map((log) => log.msg).join('\n');
  expect(combinedLogs).toContain('[link] autolinkable-package');
  expect(combinedLogs).toContain('[skip] not-autolinkable-package');
});

it('not delete packages with two hars', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        '@rnoh': {
          'multiple-har': {
            harmony: {
              'multiple_har.har': '',
              'some_other_har.har': '',
            },
            'package.json': JSON.stringify({
              name: '@rnoh/multiple-har',
              harmony: {
                autolinking: null,
              },
            }),
          },
        },
      },
      harmony: {
        ...baseFileStructure.harmony,
        'oh-package.json5': `{
            "dependencies": {
              "@rnoh/multiple-har": "file:../node_modules/@rnoh/multiple-har/harmony/multiple_har.har",
            }
          }`,
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  expect(ohPackageContent).toContain('@rnoh/multiple-har');
});

it('should preserve comments in oh-package.json5', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'test-package': {
          harmony: {
            'test_package.har': '',
          },
          'package.json': JSON.stringify({
            name: 'test-package',
            harmony: {
              autolinking: true,
            },
          }),
        },
      },
      harmony: {
        ...baseFileStructure.harmony,
        'oh-package.json5': `{
  // This is a top-level comment
  "modelVersion": "5.0.0",
  "license": "ISC",
  /* This is a multi-line
     comment */
  "dependencies": {
    // Existing dependency comment
    "@rnoh/react-native-openharmony/": "./react_native_openharmony",
  },
}`,
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  // Verify comments are preserved
  expect(ohPackageContent).toContain('// This is a top-level comment');
  expect(ohPackageContent).toContain('/* This is a multi-line');
  expect(ohPackageContent).toContain('// Existing dependency comment');
  // Verify new dependency is added
  expect(ohPackageContent).toContain('@rnoh/test-package');
});

it('should support custom mainHarPath for HAR scanning', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'custom-path-package': {
          'custom_harmony': {
            'custom.har': '',
            'subdir': {
              'nested.har': '',
            },
          },
          'package.json': JSON.stringify({
            name: 'custom-path-package',
            harmony: {
              autolinking: {
                mainHarPath: 'custom_harmony',
              },
            },
          }),
        },
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  // Should find both HAR files in custom_harmony directory (recursive scan)
  expect(ohPackageContent).toContain('@rnoh/custom-path-package--custom');
  expect(ohPackageContent).toContain('@rnoh/custom-path-package--nested');
  expect(ohPackageContent).toContain('custom_harmony/custom.har');
  expect(ohPackageContent).toContain('custom_harmony/subdir/nested.har');
});

it('should support multiple HARs with ohPackageName array mapping', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'multi-har-package': {
          harmony: {
            'core.har': '',
            'ui.har': '',
          },
          'package.json': JSON.stringify({
            name: 'multi-har-package',
            harmony: {
              autolinking: {
                ohPackageName: [
                  { harName: 'core.har', packageName: '@rnoh/multi-har--core' },
                  { harName: 'ui.har', packageName: '@rnoh/multi-har--ui' },
                ],
                etsPackageClassName: 'MultiHarPackage',
                cppPackageClassName: 'MultiHarPackage',
                cmakeLibraryTargetName: 'rnoh__multi_har',
              },
            },
          }),
        },
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  // Both HARs should be in oh-package.json5 with their custom package names
  expect(ohPackageContent).toContain('@rnoh/multi-har--core');
  expect(ohPackageContent).toContain('@rnoh/multi-har--ui');
  expect(ohPackageContent).toContain('harmony/core.har');
  expect(ohPackageContent).toContain('harmony/ui.har');

  // ETS template should use shared package class name
  const etsContent = memFS.readTextSync(output.etsRNOHPackagesFactoryPath);
  expect(etsContent).toContain('import MultiHarPackage from \'@rnoh/multi-har--core\'');
  expect(etsContent).toContain('new MultiHarPackage(ctx)');

  // C++ template should use shared package class name
  const cppContent = memFS.readTextSync(output.cppRNOHPackagesFactoryPath);
  expect(cppContent).toContain('#include "MultiHarPackage.h"');
  expect(cppContent).toContain('std::make_shared<rnoh::MultiHarPackage>(ctx)');
});

it('should use default naming with suffix for unmatched HARs when ohPackageName is array', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'partial-mapping-package': {
          harmony: {
            'matched.har': '',
            'unmatched.har': '',
          },
          'package.json': JSON.stringify({
            name: 'partial-mapping-package',
            harmony: {
              autolinking: {
                ohPackageName: [
                  { harName: 'matched.har', packageName: '@rnoh/partial--matched' },
                  // unmatched.har is not in the mapping, should use default name with suffix
                ],
              },
            },
          }),
        },
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  // matched.har should use custom package name
  expect(ohPackageContent).toContain('@rnoh/partial--matched');
  // unmatched.har should use default naming with suffix
  expect(ohPackageContent).toContain('@rnoh/partial-mapping-package--unmatched');
});

it('should support remote dependency with version field', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'remote-lib': {
          harmony: {
            'remote.har': '',
          },
          'package.json': JSON.stringify({
            name: 'remote-lib',
            harmony: {
              autolinking: {
                ohPackageName: [
                  { harName: 'remote.har', packageName: '@ohos/remote-lib', version: '1.0.0' }
                ],
                etsPackageClassName: 'RemoteLibPackage',
                cppPackageClassName: 'RemoteLibPackage',
              },
            },
          }),
        },
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  // Should use version number instead of file: path
  expect(ohPackageContent).toContain('"@ohos/remote-lib": "1.0.0"');
  expect(ohPackageContent).not.toContain('file:');

  // ETS and C++ templates should still work correctly
  const etsContent = memFS.readTextSync(output.etsRNOHPackagesFactoryPath);
  expect(etsContent).toContain('import RemoteLibPackage from \'@ohos/remote-lib\'');

  const cppContent = memFS.readTextSync(output.cppRNOHPackagesFactoryPath);
  expect(cppContent).toContain('#include "RemoteLibPackage.h"');
});

it('should support mixed local and remote dependencies in same package', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'mixed-deps-package': {
          harmony: {
            'local.har': '',
            'remote.har': '',
          },
          'package.json': JSON.stringify({
            name: 'mixed-deps-package',
            harmony: {
              autolinking: {
                ohPackageName: [
                  // Local dependency (no version)
                  { harName: 'local.har', packageName: '@rnoh/mixed--local' },
                  // Remote dependency (with version)
                  { harName: 'remote.har', packageName: '@ohos/mixed--remote', version: '2.0.0' }
                ],
                etsPackageClassName: 'MixedDepsPackage',
                cppPackageClassName: 'MixedDepsPackage',
              },
            },
          }),
        },
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  // Local dependency should use file: path
  expect(ohPackageContent).toContain('"@rnoh/mixed--local": "file:');
  expect(ohPackageContent).toContain('harmony/local.har');
  // Remote dependency should use version number
  expect(ohPackageContent).toContain('"@ohos/mixed--remote": "2.0.0"');
});

it('should support multiple remote dependencies', async () => {
  const { runAutolinking, memFS } = createAutolinking({
    fsStructure: {
      ...baseFileStructure,
      node_modules: {
        'multi-remote-package': {
          harmony: {
            'core.har': '',
            'ui.har': '',
          },
          'package.json': JSON.stringify({
            name: 'multi-remote-package',
            harmony: {
              autolinking: {
                ohPackageName: [
                  { harName: 'core.har', packageName: '@ohos/multi--core', version: '^1.0.0' },
                  { harName: 'ui.har', packageName: '@ohos/multi--ui', version: '^2.0.0' }
                ],
                etsPackageClassName: 'MultiRemotePackage',
                cppPackageClassName: 'MultiRemotePackage',
              },
            },
          }),
        },
      },
    },
  });

  const output = await runAutolinking();

  const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
  // Both should use version numbers
  expect(ohPackageContent).toContain('"@ohos/multi--core": "^1.0.0"');
  expect(ohPackageContent).toContain('"@ohos/multi--ui": "^2.0.0"');
  expect(ohPackageContent).not.toContain('file:');
});