/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @file
 * Test suite for embedded files in LIB_SYMBOL objects, including derived symbols.
 */

#include <qa_utils/wx_utils/unit_test_utils.h>

// Code under test
#include <lib_symbol.h>
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
#include <sch_io/sch_io_mgr.h>
#include <embedded_files.h>
#include <mmh3_hash.h>
#include <schematic.h>
#include <sch_screen.h>
#include <sch_sheet.h>
#include <sch_sheet_path.h>
#include <sch_symbol.h>
#include <project.h>
#include <lib_id.h>
#include <settings/settings_manager.h>

#include <wx/filename.h>
#include <wx/dir.h>

class SYMBOL_EMBEDDED_FILES_TEST_FIXTURE
{
public:
    SYMBOL_EMBEDDED_FILES_TEST_FIXTURE()
    {
        m_tempDir = wxFileName::CreateTempFileName( wxS( "kicad_test_" ) );
        wxRemoveFile( m_tempDir );
        wxFileName::Mkdir( m_tempDir );

        m_libPath = wxFileName( m_tempDir, wxS( "test_lib.kicad_sym" ) ).GetFullPath();
    }

    ~SYMBOL_EMBEDDED_FILES_TEST_FIXTURE()
    {
        // Clean up temporary directory
        if( wxFileName::DirExists( m_tempDir ) )
        {
            wxFileName::Rmdir( m_tempDir, wxPATH_RMDIR_RECURSIVE );
        }
    }

    wxString GetTempDir() const { return m_tempDir; }
    wxString GetLibPath() const { return m_libPath; }

    /**
     * Create a test embedded file with the given name and content.
     */
    EMBEDDED_FILES::EMBEDDED_FILE* CreateTestEmbeddedFile( const wxString& aName,
                                                           const std::string& aContent )
    {
        EMBEDDED_FILES::EMBEDDED_FILE* file = new EMBEDDED_FILES::EMBEDDED_FILE();
        file->name = aName;
        file->decompressedData.assign( aContent.begin(), aContent.end() );

        MMH3_HASH hash( EMBEDDED_FILES::Seed() );
        hash.add( file->decompressedData );
        file->data_hash = hash.digest().ToString();

        EMBEDDED_FILES::RETURN_CODE result = EMBEDDED_FILES::CompressAndEncode( *file );
        BOOST_REQUIRE( result == EMBEDDED_FILES::RETURN_CODE::OK );

        return file;
    }

private:
    wxString m_tempDir;
    wxString m_libPath;
};


BOOST_FIXTURE_TEST_SUITE( SymbolEmbeddedFiles, SYMBOL_EMBEDDED_FILES_TEST_FIXTURE )


/**
 * Test embedded files in parent and derived symbols:
 * 1) Create a symbol and add an embedded file and save.
 * 2) Close and reload the symbol to verify that the embedded file still exists.
 * 3) Create a derived symbol based on the first symbol and save.
 * 4) Close and re-open the derived symbol. Ensure that the embedded file does not exist
 *    in the derived symbol.
 * 5) Embed a new file in the derived symbol.
 * 6) Close and reopen the derived symbol. Ensure that the new embedded file exists.
 * 7) Add only the derived symbol to a new schematic sheet. Ensure that both embedded
 *    files are present in the schematic.
 */
BOOST_AUTO_TEST_CASE( DerivedSymbolEmbeddedFiles )
{
    // Step 1: Create a parent symbol with an embedded file and save
    std::unique_ptr<LIB_SYMBOL> parentSymbol = std::make_unique<LIB_SYMBOL>( wxS( "ParentSymbol" ) );
    parentSymbol->GetValueField().SetText( wxS( "ParentSymbol" ) );
    parentSymbol->GetReferenceField().SetText( wxS( "U" ) );

    // Add an embedded file to the parent
    EMBEDDED_FILES::EMBEDDED_FILE* parentFile =
        CreateTestEmbeddedFile( wxS( "parent_datasheet.pdf" ), "Parent datasheet content" );
    parentSymbol->AddFile( parentFile );

    BOOST_CHECK( parentSymbol->HasFile( wxS( "parent_datasheet.pdf" ) ) );
    BOOST_CHECK_EQUAL( parentSymbol->EmbeddedFileMap().size(), 1 );

    // Save the parent symbol to the library
    {
        IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
        plugin->CreateLibrary( GetLibPath() );
        plugin->SaveSymbol( GetLibPath(), new LIB_SYMBOL( *parentSymbol ) );
        plugin->SaveLibrary( GetLibPath() );
    }

    // Step 2: Close and reload the symbol to verify that the embedded file still exists.
    {
        IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
        LIB_SYMBOL* loadedParent = plugin->LoadSymbol( GetLibPath(), wxS( "ParentSymbol" ) );
        BOOST_REQUIRE( loadedParent );
        BOOST_CHECK( loadedParent->HasFile( wxS( "parent_datasheet.pdf" ) ) );
    }

    // Step 3: Create a derived symbol based on the first symbol and save
    {
        IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
        LIB_SYMBOL* loadedParent = plugin->LoadSymbol( GetLibPath(), wxS( "ParentSymbol" ) );
        BOOST_REQUIRE( loadedParent );

        std::unique_ptr<LIB_SYMBOL> derivedSymbol = std::make_unique<LIB_SYMBOL>( wxS( "DerivedSymbol" ) );
        derivedSymbol->GetValueField().SetText( wxS( "DerivedSymbol" ) );
        derivedSymbol->SetParent( loadedParent );

        plugin->SaveSymbol( GetLibPath(), new LIB_SYMBOL( *derivedSymbol ) );
        plugin->SaveLibrary( GetLibPath() );
    }

    // Step 4: Close and re-open the derived symbol. Ensure that the embedded file does not exist in the derived symbol.
    {
        IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
        LIB_SYMBOL* loadedDerived = plugin->LoadSymbol( GetLibPath(), wxS( "DerivedSymbol" ) );
        BOOST_REQUIRE( loadedDerived );

        // The derived symbol itself should not have the file in its map
        BOOST_CHECK( !loadedDerived->HasFile( wxS( "parent_datasheet.pdf" ) ) );
        BOOST_CHECK_EQUAL( loadedDerived->EmbeddedFileMap().size(), 0 );
    }

    // Step 5: Embed a new file in the derived symbol
    {
        IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
        // We need to load parent to set it again, because LoadSymbol doesn't automatically link parents from disk without a library table
        LIB_SYMBOL* loadedParent = plugin->LoadSymbol( GetLibPath(), wxS( "ParentSymbol" ) );
        LIB_SYMBOL* loadedDerived = plugin->LoadSymbol( GetLibPath(), wxS( "DerivedSymbol" ) );
        BOOST_REQUIRE( loadedDerived );
        loadedDerived->SetParent( loadedParent );

        EMBEDDED_FILES::EMBEDDED_FILE* derivedFile =
            CreateTestEmbeddedFile( wxS( "derived_datasheet.pdf" ), "Derived datasheet content" );
        loadedDerived->AddFile( derivedFile );

        BOOST_CHECK( loadedDerived->HasFile( wxS( "derived_datasheet.pdf" ) ) );

        plugin->SaveSymbol( GetLibPath(), new LIB_SYMBOL( *loadedDerived ) );
        plugin->SaveLibrary( GetLibPath() );
    }

    // Step 6: Close and reopen the derived symbol. Ensure that the new embedded file exists
    {
        IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
        LIB_SYMBOL* loadedDerived = plugin->LoadSymbol( GetLibPath(), wxS( "DerivedSymbol" ) );
        BOOST_REQUIRE( loadedDerived );
        BOOST_CHECK( loadedDerived->HasFile( wxS( "derived_datasheet.pdf" ) ) );
    }

    // Step 7: Add only the derived symbol to a new schematic sheet. Ensure that both embedded files are present in the schematic.
    {
        SCHEMATIC schematic( nullptr );

        schematic.Reset();
        SCH_SHEET* defaultSheet = schematic.GetTopLevelSheet( 0 );

        SCH_SHEET* rootSheet = new SCH_SHEET( &schematic );
        SCH_SCREEN* screen = new SCH_SCREEN( &schematic );
        rootSheet->SetScreen( screen );

        schematic.AddTopLevelSheet( rootSheet );
        schematic.RemoveTopLevelSheet( defaultSheet );
        delete defaultSheet;

        IO_RELEASER<SCH_IO> plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
        LIB_SYMBOL* loadedParent = plugin->LoadSymbol( GetLibPath(), wxS( "ParentSymbol" ) );
        LIB_SYMBOL* loadedDerived = plugin->LoadSymbol( GetLibPath(), wxS( "DerivedSymbol" ) );

        loadedDerived->SetParent( loadedParent );

        SCH_SHEET_PATH rootPath;
        rootPath.push_back( &schematic.Root() );
        rootPath.push_back( rootSheet );

        SCH_SYMBOL* schSymbol = new SCH_SYMBOL( *loadedDerived, LIB_ID( "test_lib", "DerivedSymbol" ),
                                                &rootPath, 0 );
        screen->Append( schSymbol );

        // Verify derived file
        BOOST_CHECK( schSymbol->GetLibSymbolRef()->HasFile( wxS( "derived_datasheet.pdf" ) ) );

        // Verify parent file (should be flattened into the symbol)
        BOOST_CHECK( schSymbol->GetLibSymbolRef()->HasFile( wxS( "parent_datasheet.pdf" ) ) );
    }
}


/**
 * Test that copying a symbol with embedded files preserves the files.
 */
BOOST_AUTO_TEST_CASE( CopySymbolPreservesEmbeddedFiles )
{
    // Create a symbol with an embedded file
    std::unique_ptr<LIB_SYMBOL> original = std::make_unique<LIB_SYMBOL>( wxS( "Original" ) );
    original->GetValueField().SetText( wxS( "Original" ) );

    EMBEDDED_FILES::EMBEDDED_FILE* file =
        CreateTestEmbeddedFile( wxS( "test.pdf" ), "Test content" );
    original->AddFile( file );

    BOOST_CHECK_EQUAL( original->EmbeddedFileMap().size(), 1 );

    // Copy the symbol using copy constructor
    std::unique_ptr<LIB_SYMBOL> copy = std::make_unique<LIB_SYMBOL>( *original );

    // Verify the copy has the embedded file
    BOOST_CHECK_EQUAL( copy->EmbeddedFileMap().size(), 1 );
    BOOST_CHECK( copy->HasFile( wxS( "test.pdf" ) ) );

    EMBEDDED_FILES::EMBEDDED_FILE* copiedFile = copy->GetEmbeddedFile( wxS( "test.pdf" ) );
    BOOST_REQUIRE( copiedFile );
    BOOST_CHECK( copiedFile->Validate() );
}


/**
 * Test that assignment operator preserves embedded files.
 */
BOOST_AUTO_TEST_CASE( AssignmentPreservesEmbeddedFiles )
{
    // Create a symbol with an embedded file
    std::unique_ptr<LIB_SYMBOL> source = std::make_unique<LIB_SYMBOL>( wxS( "Source" ) );
    source->GetValueField().SetText( wxS( "Source" ) );

    EMBEDDED_FILES::EMBEDDED_FILE* file =
        CreateTestEmbeddedFile( wxS( "source.pdf" ), "Source content" );
    source->AddFile( file );

    // Create a destination symbol
    std::unique_ptr<LIB_SYMBOL> dest = std::make_unique<LIB_SYMBOL>( wxS( "Dest" ) );
    dest->GetValueField().SetText( wxS( "Dest" ) );

    // Assign source to destination
    *dest = *source;

    // Verify destination has the embedded file
    BOOST_CHECK_EQUAL( dest->EmbeddedFileMap().size(), 1 );
    BOOST_CHECK( dest->HasFile( wxS( "source.pdf" ) ) );

    EMBEDDED_FILES::EMBEDDED_FILE* destFile = dest->GetEmbeddedFile( wxS( "source.pdf" ) );
    BOOST_REQUIRE( destFile );
    BOOST_CHECK( destFile->Validate() );
}


/**
 * Adding a new embedded file to a placed schematic symbol must not drop the existing files.
 * The schematic serializes the screen's cached library symbols, while the symbol properties
 * dialog edits the placed instance's library symbol.  SCH_SCREEN::Append must reconcile the
 * cache with the instance even when only the embedded files changed (LIB_SYMBOL::Compare
 * ignores them); without it the edits are lost on save.
 */
BOOST_AUTO_TEST_CASE( AddEmbeddedFileToPlacedSymbolSurvivesSave )
{
    SETTINGS_MANAGER settingsManager;
    wxString         projectPath = wxFileName( GetTempDir(), wxS( "test_project.kicad_pro" ) ).GetFullPath();
    settingsManager.LoadProject( projectPath.ToStdString() );

    auto schematic = std::make_unique<SCHEMATIC>( nullptr );
    schematic->SetProject( &settingsManager.Prj() );
    schematic->Reset();

    SCH_SHEET*  defaultSheet = schematic->GetTopLevelSheet( 0 );
    SCH_SHEET*  rootSheet = new SCH_SHEET( schematic.get() );
    SCH_SCREEN* screen = new SCH_SCREEN( schematic.get() );
    rootSheet->SetScreen( screen );

    schematic->AddTopLevelSheet( rootSheet );
    schematic->RemoveTopLevelSheet( defaultSheet );
    delete defaultSheet;

    // Build a library symbol carrying one embedded file and place it on the sheet.
    std::unique_ptr<LIB_SYMBOL> libSymbol = std::make_unique<LIB_SYMBOL>( wxS( "EmbeddedSymbol" ) );
    libSymbol->GetValueField().SetText( wxS( "EmbeddedSymbol" ) );
    libSymbol->GetReferenceField().SetText( wxS( "U" ) );
    libSymbol->AddFile( CreateTestEmbeddedFile( wxS( "datasheet.pdf" ), "Original datasheet" ) );

    SCH_SHEET_PATH rootPath;
    rootPath.push_back( &schematic->Root() );
    rootPath.push_back( rootSheet );

    // Two instances share one cached library symbol.  This is the case that exposes the bug:
    // removing one symbol keeps the cache entry alive, so Append must reconcile the cache
    // through its "unchanged" branch (which is what LIB_SYMBOL::Compare reports for an
    // embedded-file-only edit) rather than rebuilding it from scratch.
    SCH_SYMBOL* schSymbol =
            new SCH_SYMBOL( *libSymbol, LIB_ID( "test_lib", "EmbeddedSymbol" ), &rootPath, 0 );
    schSymbol->SetRef( &rootPath, wxS( "U1" ) );
    screen->Append( schSymbol );

    SCH_SYMBOL* otherSymbol =
            new SCH_SYMBOL( *libSymbol, LIB_ID( "test_lib", "EmbeddedSymbol" ), &rootPath, 0 );
    otherSymbol->SetRef( &rootPath, wxS( "U2" ) );
    otherSymbol->Move( VECTOR2I( 2540000, 0 ) );
    screen->Append( otherSymbol );

    // Adding a second file through the properties dialog only touches the placed instance's
    // library symbol, mirroring what PANEL_EMBEDDED_FILES does.
    schSymbol->GetEmbeddedFiles()->AddFile( CreateTestEmbeddedFile( wxS( "extra.pdf" ), "Newly added" ) );

    BOOST_CHECK( schSymbol->GetEmbeddedFiles()->HasFile( wxS( "datasheet.pdf" ) ) );
    BOOST_CHECK( schSymbol->GetEmbeddedFiles()->HasFile( wxS( "extra.pdf" ) ) );

    // Re-add the symbol the same way the properties dialog does on OK; Append must push the
    // instance's embedded files into the cache that is serialized.
    BOOST_REQUIRE( screen->Remove( schSymbol ) );
    screen->Append( schSymbol );

    wxString fileName = wxFileName( GetTempDir(), wxS( "embedded_roundtrip.kicad_sch" ) ).GetFullPath();

    SCH_IO_KICAD_SEXPR io;
    io.SaveSchematicFile( fileName, rootSheet, schematic.get() );
    BOOST_REQUIRE( wxFileExists( fileName ) );

    schematic->Reset();
    SCH_SHEET* newDefaultSheet = schematic->GetTopLevelSheet( 0 );
    SCH_SHEET* loadedSheet = io.LoadSchematicFile( fileName, schematic.get() );
    BOOST_REQUIRE( loadedSheet );

    schematic->AddTopLevelSheet( loadedSheet );
    schematic->RemoveTopLevelSheet( newDefaultSheet );
    delete newDefaultSheet;

    SCH_SCREEN* loadedScreen = loadedSheet->GetScreen();
    BOOST_REQUIRE( loadedScreen );

    bool foundSymbol = false;

    for( const auto& [name, cachedSymbol] : loadedScreen->GetLibSymbols() )
    {
        if( cachedSymbol->GetName() == wxS( "EmbeddedSymbol" ) )
        {
            foundSymbol = true;
            BOOST_CHECK( cachedSymbol->HasFile( wxS( "datasheet.pdf" ) ) );
            BOOST_CHECK( cachedSymbol->HasFile( wxS( "extra.pdf" ) ) );
        }
    }

    BOOST_CHECK( foundSymbol );
}


BOOST_AUTO_TEST_SUITE_END()
